From 19af2baec434b52705bbf7f19aeaffe73423e638 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:12:48 +0100 Subject: [PATCH 01/21] fix(options): restore compatible generated APIs --- docs/docs/mp-packages/cli/kustomize.md | 30 +++++++++++-------- .../Options/BuildahBuildOptions.Generated.cs | 5 ++-- .../Options/BuildahFromOptions.Generated.cs | 5 ++-- .../BuildahManifestOptions.Generated.cs | 6 ++++ .../Options/BuildahSourceOptions.Generated.cs | 6 ++++ .../DockerComposeBuildOptions.Generated.cs | 14 ++++++++- .../DockerComposeConfigOptions.Generated.cs | 6 ++++ .../DockerComposeEventsOptions.Generated.cs | 12 ++++++++ .../DockerComposeExecOptions.Generated.cs | 6 ++++ .../DockerComposePublishOptions.Generated.cs | 6 ++++ .../DockerComposeStartOptions.Generated.cs | 12 ++++++++ .../DockerComposeUpOptions.Generated.cs | 6 ++++ .../Generated/Flux.CommandCoverage.json | 2 +- ...otstrapBitbucketServerOptions.Generated.cs | 1 + .../FluxBootstrapGitOptions.Generated.cs | 1 + .../FluxBootstrapGiteaOptions.Generated.cs | 1 + .../FluxBootstrapGithubOptions.Generated.cs | 1 + .../FluxBootstrapGitlabOptions.Generated.cs | 1 + .../Options/FluxBootstrapOptions.Generated.cs | 7 +++++ .../Options/FluxBuildOptions.Generated.cs | 6 ++++ .../FluxCreateImageOptions.Generated.cs | 6 ++++ .../Options/FluxCreateOptions.Generated.cs | 6 ++++ .../FluxCreateSecretGitOptions.Generated.cs | 1 + .../FluxCreateSecretOptions.Generated.cs | 6 ++++ .../FluxCreateSourceGitOptions.Generated.cs | 1 + .../FluxCreateSourceOptions.Generated.cs | 6 ++++ .../Options/FluxDebugOptions.Generated.cs | 6 ++++ .../FluxDeleteImageOptions.Generated.cs | 6 ++++ .../Options/FluxDeleteOptions.Generated.cs | 6 ++++ .../FluxDeleteSourceOptions.Generated.cs | 6 ++++ .../Options/FluxDiffOptions.Generated.cs | 6 ++++ .../FluxExportArtifactOptions.Generated.cs | 6 ++++ .../FluxExportImageOptions.Generated.cs | 6 ++++ .../Options/FluxExportOptions.Generated.cs | 6 ++++ .../FluxExportSourceOptions.Generated.cs | 6 ++++ .../FluxGetArtifactsOptions.Generated.cs | 6 ++++ .../Options/FluxGetImagesOptions.Generated.cs | 6 ++++ .../Options/FluxGetOptions.Generated.cs | 6 ++++ .../FluxGetSourcesOptions.Generated.cs | 6 ++++ .../Options/FluxListOptions.Generated.cs | 6 ++++ .../FluxPluginInstallOptions.Generated.cs | 2 +- .../Options/FluxPluginOptions.Generated.cs | 6 ++++ .../Options/FluxPullOptions.Generated.cs | 6 ++++ .../Options/FluxPushOptions.Generated.cs | 6 ++++ .../FluxReconcileImageOptions.Generated.cs | 6 ++++ .../Options/FluxReconcileOptions.Generated.cs | 6 ++++ .../FluxReconcileSourceOptions.Generated.cs | 6 ++++ .../FluxResumeImageOptions.Generated.cs | 6 ++++ .../Options/FluxResumeOptions.Generated.cs | 6 ++++ .../FluxResumeSourceOptions.Generated.cs | 6 ++++ .../FluxSuspendImageOptions.Generated.cs | 6 ++++ .../Options/FluxSuspendOptions.Generated.cs | 6 ++++ .../FluxSuspendSourceOptions.Generated.cs | 6 ++++ .../Options/FluxTagOptions.Generated.cs | 6 ++++ .../FluxTreeArtifactOptions.Generated.cs | 6 ++++ .../Options/FluxTreeOptions.Generated.cs | 6 ++++ .../Options/FluxTriggerOptions.Generated.cs | 6 ++++ .../Options/GrypeDbOptions.Generated.cs | 6 ++++ .../Options/GrypeDbSearchOptions.Generated.cs | 6 ++++ .../KustomizeExtensions.Generated.cs | 2 +- .../KustomizeCfgCatOptions.Generated.cs | 4 +-- .../KustomizeCfgCountOptions.Generated.cs | 3 -- .../KustomizeCfgGrepOptions.Generated.cs | 9 ++---- .../KustomizeCfgTreeOptions.Generated.cs | 3 -- .../KustomizeCreateOptions.Generated.cs | 4 +-- ...zeEditAddBuildmetadataOptions.Generated.cs | 4 +-- ...tomizeEditAddConfigmapOptions.Generated.cs | 6 ++-- ...KustomizeEditAddSecretOptions.Generated.cs | 6 ++-- ...ditRemoveBuildmetadataOptions.Generated.cs | 4 +-- ...izeEditRemoveConfigmapOptions.Generated.cs | 4 +-- ...tomizeEditRemoveSecretOptions.Generated.cs | 4 +-- ...zeEditSetBuildmetadataOptions.Generated.cs | 4 +-- ...tomizeEditSetConfigmapOptions.Generated.cs | 4 +-- ...KustomizeEditSetSecretOptions.Generated.cs | 4 +-- .../KustomizeFnRunOptions.Generated.cs | 3 -- .../KustomizeLocalizeOptions.Generated.cs | 12 -------- .../Services/IKustomizeCfg.Generated.cs | 7 ++--- .../Services/IKustomizeEdit.Generated.cs | 3 -- .../Services/IKustomizeFn.Generated.cs | 3 -- .../Services/KustomizeCfg.Generated.cs | 8 ++--- .../Services/KustomizeEditAdd.Generated.cs | 27 ++++------------- .../Services/KustomizeEditRemove.Generated.cs | 27 ++++------------- .../Services/KustomizeEditSet.Generated.cs | 27 ++++------------- .../Generated/Pulumi.CommandCoverage.json | 2 +- .../PulumiDeploymentRunOptions.Generated.cs | 1 + .../Options/PulumiDestroyOptions.Generated.cs | 11 ++----- .../Options/PulumiEnvDiffOptions.Generated.cs | 9 ++++-- .../Options/PulumiEnvEditOptions.Generated.cs | 3 +- ...nvProviderAwsLoginOidcOptions.Generated.cs | 9 +----- ...ProviderAwsLoginStaticOptions.Generated.cs | 9 +----- ...ProviderAzureLoginOidcOptions.Generated.cs | 9 +----- ...oviderAzureLoginStaticOptions.Generated.cs | 9 +----- ...nvProviderGcpLoginOidcOptions.Generated.cs | 9 +----- ...ProviderGcpLoginStaticOptions.Generated.cs | 9 +----- .../Options/PulumiEnvSetOptions.Generated.cs | 3 +- .../Options/PulumiImportOptions.Generated.cs | 3 +- .../Options/PulumiNewOptions.Generated.cs | 18 +++++++++++ .../PulumiPackageAddOptions.Generated.cs | 6 ---- .../PulumiPackageGenSdkOptions.Generated.cs | 6 ---- ...ulumiPackageGetMappingOptions.Generated.cs | 6 ---- ...PulumiPackageGetSchemaOptions.Generated.cs | 6 ---- .../PulumiPackageInfoOptions.Generated.cs | 6 ---- .../PulumiPackagePublishOptions.Generated.cs | 6 ---- .../PulumiPluginInstallOptions.Generated.cs | 16 ++-------- .../Options/PulumiPreviewOptions.Generated.cs | 13 ++------ .../PulumiProjectNewOptions.Generated.cs | 18 +++++++++++ .../Options/PulumiRefreshOptions.Generated.cs | 3 +- .../PulumiStackNewOptions.Generated.cs | 10 +++++-- .../Options/PulumiUpOptions.Generated.cs | 13 ++------ .../Services/IPulumiStack.Generated.cs | 2 +- .../Services/PulumiStack.Generated.cs | 4 +-- .../Options/CargoAddOptions.Generated.cs | 2 +- .../Options/SkopeoCopyOptions.Generated.cs | 3 +- .../Options/SkopeoDeleteOptions.Generated.cs | 2 +- ...peoGenerateSigstoreKeyOptions.Generated.cs | 3 +- .../Options/SkopeoInspectOptions.Generated.cs | 2 +- .../SkopeoListTagsOptions.Generated.cs | 2 +- .../Options/SkopeoLoginOptions.Generated.cs | 2 +- .../Options/SkopeoLogoutOptions.Generated.cs | 2 +- .../SkopeoStandaloneSignOptions.Generated.cs | 3 +- .../Options/SkopeoSyncOptions.Generated.cs | 3 +- .../TrivyFilesystemOptions.Generated.cs | 1 + .../Options/TrivyImageOptions.Generated.cs | 1 + .../TrivyKubernetesOptions.Generated.cs | 1 + .../Options/TrivyModuleOptions.Generated.cs | 6 ++++ .../Options/TrivyPluginOptions.Generated.cs | 6 ++++ .../Options/TrivyRegistryOptions.Generated.cs | 6 ++++ .../TrivyRepositoryOptions.Generated.cs | 1 + .../Options/TrivyRootfsOptions.Generated.cs | 1 + .../Options/TrivyVexOptions.Generated.cs | 6 ++++ .../Options/TrivyVexRepoOptions.Generated.cs | 6 ++++ .../Options/TrivyVmOptions.Generated.cs | 1 + 132 files changed, 498 insertions(+), 317 deletions(-) diff --git a/docs/docs/mp-packages/cli/kustomize.md b/docs/docs/mp-packages/cli/kustomize.md index 30bae4e5bf9..5ab193ef75e 100644 --- a/docs/docs/mp-packages/cli/kustomize.md +++ b/docs/docs/mp-packages/cli/kustomize.md @@ -7,13 +7,7 @@ title: kustomize CLI reference `ModularPipelines.Kubernetes` provides strongly typed access to the `kustomize` CLI. -## Executable prerequisite - -This package does not install the `kustomize` executable. Install it separately and ensure `kustomize` is available on `PATH`. - -Follow the executable's official documentation for installation instructions. - -## Package installation +## Installation ```shell dotnet add package ModularPipelines.Kubernetes @@ -23,10 +17,23 @@ Resolve the service with `context.Tools.Kustomize`. For projects older than C# 1 ## Module example -Resolve the service in a module, then select a command from the table below. A runnable example is omitted when no command has complete safety metadata: - ```csharp -var kustomize = context.Tools.Kustomize; +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; +using ModularPipelines.Kubernetes.Options; + +public class RunCommandModule : Module +{ + protected override async Task ExecuteAsync( + IModuleContext context, + CancellationToken cancellationToken) + { + return await context.Tools.Kustomize.BuildAsync( + new KustomizeBuildOptions(), + cancellationToken: cancellationToken); + } +} ``` ## Commands @@ -34,13 +41,11 @@ var kustomize = context.Tools.Kustomize; | CLI command | Options record | | --- | --- | | `kustomize build` | `KustomizeBuildOptions` | -| `kustomize cfg` | `KustomizeCfgOptions` | | `kustomize cfg cat` | `KustomizeCfgCatOptions` | | `kustomize cfg count` | `KustomizeCfgCountOptions` | | `kustomize cfg grep` | `KustomizeCfgGrepOptions` | | `kustomize cfg tree` | `KustomizeCfgTreeOptions` | | `kustomize create` | `KustomizeCreateOptions` | -| `kustomize edit` | `KustomizeEditOptions` | | `kustomize edit add` | `KustomizeEditAddOptions` | | `kustomize edit add annotation` | `KustomizeEditAddAnnotationOptions` | | `kustomize edit add base` | `KustomizeEditAddBaseOptions` | @@ -75,6 +80,5 @@ var kustomize = context.Tools.Kustomize; | `kustomize edit set namesuffix` | `KustomizeEditSetNamesuffixOptions` | | `kustomize edit set replicas` | `KustomizeEditSetReplicasOptions` | | `kustomize edit set secret` | `KustomizeEditSetSecretOptions` | -| `kustomize fn` | `KustomizeFnOptions` | | `kustomize fn run` | `KustomizeFnRunOptions` | | `kustomize localize` | `KustomizeLocalizeOptions` | diff --git a/src/ModularPipelines.Buildah/Options/BuildahBuildOptions.Generated.cs b/src/ModularPipelines.Buildah/Options/BuildahBuildOptions.Generated.cs index 85383314139..69ef63b69b4 100644 --- a/src/ModularPipelines.Buildah/Options/BuildahBuildOptions.Generated.cs +++ b/src/ModularPipelines.Buildah/Options/BuildahBuildOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Buildah.Options; -using ModularPipelines.Models; namespace ModularPipelines.Buildah.Options; @@ -414,8 +413,8 @@ public record BuildahBuildOptions : BuildahOptions /// /// pull the image from the registry if newer or not present in store, if false, only pull the image if not present, if always, pull the image even if the named image is present in store, if never, only use the image present in store if available (default "true") /// - [CliOption("--pull", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? Pull { get; set; } + [CliOption("--pull", Format = OptionFormat.EqualsSeparated)] + public string? Pull { get; set; } /// /// refrain from announcing build instructions and image read/write progress diff --git a/src/ModularPipelines.Buildah/Options/BuildahFromOptions.Generated.cs b/src/ModularPipelines.Buildah/Options/BuildahFromOptions.Generated.cs index 8f00a441032..15d720b6b7c 100644 --- a/src/ModularPipelines.Buildah/Options/BuildahFromOptions.Generated.cs +++ b/src/ModularPipelines.Buildah/Options/BuildahFromOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Buildah.Options; -using ModularPipelines.Models; namespace ModularPipelines.Buildah.Options; @@ -222,8 +221,8 @@ public record BuildahFromOptions : BuildahOptions /// /// pull the image from the registry if newer or not present in store, if false, only pull the image if not present, if always, pull the image even if the named image is present in store, if never, only use the image present in store if available (default "true") /// - [CliOption("--pull", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? Pull { get; set; } + [CliOption("--pull", Format = OptionFormat.EqualsSeparated)] + public string? Pull { get; set; } /// /// don't output progress information when pulling images diff --git a/src/ModularPipelines.Buildah/Options/BuildahManifestOptions.Generated.cs b/src/ModularPipelines.Buildah/Options/BuildahManifestOptions.Generated.cs index 38d089b2a1b..6211340776f 100644 --- a/src/ModularPipelines.Buildah/Options/BuildahManifestOptions.Generated.cs +++ b/src/ModularPipelines.Buildah/Options/BuildahManifestOptions.Generated.cs @@ -26,4 +26,10 @@ public record BuildahManifestOptions : BuildahOptions [CliFlag("--help", ShortForm = "-h")] public bool? Help { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Buildah/Options/BuildahSourceOptions.Generated.cs b/src/ModularPipelines.Buildah/Options/BuildahSourceOptions.Generated.cs index f433446fd6f..9904cfeb9b7 100644 --- a/src/ModularPipelines.Buildah/Options/BuildahSourceOptions.Generated.cs +++ b/src/ModularPipelines.Buildah/Options/BuildahSourceOptions.Generated.cs @@ -26,4 +26,10 @@ public record BuildahSourceOptions : BuildahOptions [CliFlag("--help", ShortForm = "-h")] public bool? Help { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Docker/Options/DockerComposeBuildOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposeBuildOptions.Generated.cs index 4d2aade70d5..28770b259fe 100644 --- a/src/ModularPipelines.Docker/Options/DockerComposeBuildOptions.Generated.cs +++ b/src/ModularPipelines.Docker/Options/DockerComposeBuildOptions.Generated.cs @@ -64,6 +64,12 @@ public record DockerComposeBuildOptions : DockerOptions [CliFlag("--print")] public bool? Print { get; set; } + /// + /// Add a provenance attestation + /// + [CliOption("--provenance", Format = OptionFormat.EqualsSeparated)] + public string? Provenance { get; set; } + /// /// Always attempt to pull a newer version of the image /// @@ -77,11 +83,17 @@ public record DockerComposeBuildOptions : DockerOptions public bool? Push { get; set; } /// - /// Don't print anything to STDOUT + /// Suppress the build output /// [CliFlag("--quiet", ShortForm = "-q")] public bool? Quiet { get; set; } + /// + /// Add a SBOM attestation + /// + [CliOption("--sbom", Format = OptionFormat.EqualsSeparated)] + public string? Sbom { get; set; } + /// /// Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent) /// diff --git a/src/ModularPipelines.Docker/Options/DockerComposeConfigOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposeConfigOptions.Generated.cs index 92dff38b2f0..93609f88323 100644 --- a/src/ModularPipelines.Docker/Options/DockerComposeConfigOptions.Generated.cs +++ b/src/ModularPipelines.Docker/Options/DockerComposeConfigOptions.Generated.cs @@ -56,6 +56,12 @@ public record DockerComposeConfigOptions : DockerOptions [CliFlag("--lock-image-digests")] public bool? LockImageDigests { get; set; } + /// + /// Print the model names, one per line. + /// + [CliFlag("--models")] + public bool? Models { get; set; } + /// /// Print the network names, one per line. /// diff --git a/src/ModularPipelines.Docker/Options/DockerComposeEventsOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposeEventsOptions.Generated.cs index c632088f67f..636283a7566 100644 --- a/src/ModularPipelines.Docker/Options/DockerComposeEventsOptions.Generated.cs +++ b/src/ModularPipelines.Docker/Options/DockerComposeEventsOptions.Generated.cs @@ -32,6 +32,18 @@ public record DockerComposeEventsOptions : DockerOptions [CliFlag("--json")] public bool? Json { get; set; } + /// + /// Show all events created since timestamp + /// + [CliOption("--since", Format = OptionFormat.EqualsSeparated)] + public string? Since { get; set; } + + /// + /// Stream events until this timestamp + /// + [CliOption("--until", Format = OptionFormat.EqualsSeparated)] + public string? Until { get; set; } + /// /// The SERVICE operand. /// diff --git a/src/ModularPipelines.Docker/Options/DockerComposeExecOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposeExecOptions.Generated.cs index 78f65bfb153..d4d7342e709 100644 --- a/src/ModularPipelines.Docker/Options/DockerComposeExecOptions.Generated.cs +++ b/src/ModularPipelines.Docker/Options/DockerComposeExecOptions.Generated.cs @@ -47,6 +47,12 @@ public record DockerComposeExecOptions( [CliOption("--index", Format = OptionFormat.EqualsSeparated)] public int? Index { get; set; } + /// + /// Disable pseudo-TTY allocation. By default 'docker compose exec' allocates a TTY. (default true) + /// + [CliOption("--no-tty", ShortForm = "-T", Format = OptionFormat.EqualsSeparated)] + public bool? NoTty { get; set; } + /// /// Give extended privileges to the process /// diff --git a/src/ModularPipelines.Docker/Options/DockerComposePublishOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposePublishOptions.Generated.cs index 8fe684bd950..85274911354 100644 --- a/src/ModularPipelines.Docker/Options/DockerComposePublishOptions.Generated.cs +++ b/src/ModularPipelines.Docker/Options/DockerComposePublishOptions.Generated.cs @@ -22,6 +22,12 @@ public record DockerComposePublishOptions( [property: CliArgument(0, Phase = CommandLinePhase.Passthrough, Required = true)] string RepositoryTag ) : DockerOptions { + /// + /// Published compose application (includes referenced images) + /// + [CliFlag("--app")] + public bool? App { get; set; } + /// /// Execute command in dry run mode /// diff --git a/src/ModularPipelines.Docker/Options/DockerComposeStartOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposeStartOptions.Generated.cs index e5be419e028..6dc13cf176e 100644 --- a/src/ModularPipelines.Docker/Options/DockerComposeStartOptions.Generated.cs +++ b/src/ModularPipelines.Docker/Options/DockerComposeStartOptions.Generated.cs @@ -26,6 +26,18 @@ public record DockerComposeStartOptions : DockerOptions [CliFlag("--dry-run")] public bool? DryRun { get; set; } + /// + /// Wait for services to be running|healthy. Implies detached mode. + /// + [CliFlag("--wait")] + public bool? Wait { get; set; } + + /// + /// Maximum duration in seconds to wait for the project to be running|healthy + /// + [CliOption("--wait-timeout", Format = OptionFormat.EqualsSeparated)] + public int? WaitTimeout { get; set; } + /// /// The SERVICE operand. /// diff --git a/src/ModularPipelines.Docker/Options/DockerComposeUpOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposeUpOptions.Generated.cs index da6481c60d0..bafaedf8179 100644 --- a/src/ModularPipelines.Docker/Options/DockerComposeUpOptions.Generated.cs +++ b/src/ModularPipelines.Docker/Options/DockerComposeUpOptions.Generated.cs @@ -134,6 +134,12 @@ public record DockerComposeUpOptions : DockerOptions [CliOption("--pull", Format = OptionFormat.EqualsSeparated)] public string? Pull { get; set; } + /// + /// Suppress the build output + /// + [CliFlag("--quiet-build")] + public bool? QuietBuild { get; set; } + /// /// Pull without printing progress information /// diff --git a/src/ModularPipelines.Flux/Generated/Flux.CommandCoverage.json b/src/ModularPipelines.Flux/Generated/Flux.CommandCoverage.json index a89a01db140..3af1d828b67 100644 --- a/src/ModularPipelines.Flux/Generated/Flux.CommandCoverage.json +++ b/src/ModularPipelines.Flux/Generated/Flux.CommandCoverage.json @@ -1,7 +1,7 @@ { "formatVersion": 1, "toolName": "flux", - "toolVersion": "flux version 2.9.4", + "toolVersion": "flux version 2.9.3", "commandCount": 173, "commandTreeSha256": "cce64ef09e13559016b9deee8b350b34916ce2e46c6e248c25ab24ae62a04c00", "commands": [ diff --git a/src/ModularPipelines.Flux/Options/FluxBootstrapBitbucketServerOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBootstrapBitbucketServerOptions.Generated.cs index cd468a7026d..748fcfdb41a 100644 --- a/src/ModularPipelines.Flux/Options/FluxBootstrapBitbucketServerOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxBootstrapBitbucketServerOptions.Generated.cs @@ -290,6 +290,7 @@ public record FluxBootstrapBitbucketServerOptions : FluxOptions /// /// path to a private key file used for authenticating to the Git SSH server /// + [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxBootstrapGitOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBootstrapGitOptions.Generated.cs index 6ef0774c391..d0ea2f8d0e1 100644 --- a/src/ModularPipelines.Flux/Options/FluxBootstrapGitOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxBootstrapGitOptions.Generated.cs @@ -273,6 +273,7 @@ public record FluxBootstrapGitOptions : FluxOptions /// /// path to a private key file used for authenticating to the Git SSH server /// + [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxBootstrapGiteaOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBootstrapGiteaOptions.Generated.cs index e260dc6c3c6..ec7a21189d2 100644 --- a/src/ModularPipelines.Flux/Options/FluxBootstrapGiteaOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxBootstrapGiteaOptions.Generated.cs @@ -284,6 +284,7 @@ public record FluxBootstrapGiteaOptions : FluxOptions /// /// path to a private key file used for authenticating to the Git SSH server /// + [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxBootstrapGithubOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBootstrapGithubOptions.Generated.cs index ba2d567f5e8..a073c0541d5 100644 --- a/src/ModularPipelines.Flux/Options/FluxBootstrapGithubOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxBootstrapGithubOptions.Generated.cs @@ -284,6 +284,7 @@ public record FluxBootstrapGithubOptions : FluxOptions /// /// path to a private key file used for authenticating to the Git SSH server /// + [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxBootstrapGitlabOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBootstrapGitlabOptions.Generated.cs index 01857ddffc2..db539dd827d 100644 --- a/src/ModularPipelines.Flux/Options/FluxBootstrapGitlabOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxBootstrapGitlabOptions.Generated.cs @@ -290,6 +290,7 @@ public record FluxBootstrapGitlabOptions : FluxOptions /// /// path to a private key file used for authenticating to the Git SSH server /// + [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxBootstrapOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBootstrapOptions.Generated.cs index 49b05b8131d..3e355237676 100644 --- a/src/ModularPipelines.Flux/Options/FluxBootstrapOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxBootstrapOptions.Generated.cs @@ -122,6 +122,7 @@ public record FluxBootstrapOptions : FluxOptions /// /// path to a private key file used for authenticating to the Git SSH server /// + [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } @@ -368,4 +369,10 @@ public record FluxBootstrapOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxBuildOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBuildOptions.Generated.cs index 7d35a3da902..d84274dbe60 100644 --- a/src/ModularPipelines.Flux/Options/FluxBuildOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxBuildOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxBuildOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxCreateImageOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxCreateImageOptions.Generated.cs index f7244886299..5e5055b3039 100644 --- a/src/ModularPipelines.Flux/Options/FluxCreateImageOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxCreateImageOptions.Generated.cs @@ -183,4 +183,10 @@ public record FluxCreateImageOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxCreateOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxCreateOptions.Generated.cs index c6dd4d1873d..2c11bb5b413 100644 --- a/src/ModularPipelines.Flux/Options/FluxCreateOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxCreateOptions.Generated.cs @@ -183,4 +183,10 @@ public record FluxCreateOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxCreateSecretGitOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxCreateSecretGitOptions.Generated.cs index ccb7a79f150..b2c134da88f 100644 --- a/src/ModularPipelines.Flux/Options/FluxCreateSecretGitOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxCreateSecretGitOptions.Generated.cs @@ -50,6 +50,7 @@ public record FluxCreateSecretGitOptions : FluxOptions /// /// path to a passwordless private key file used for authenticating to the Git SSH server /// + [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxCreateSecretOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxCreateSecretOptions.Generated.cs index 74c3ced94cc..1e2a16c1e57 100644 --- a/src/ModularPipelines.Flux/Options/FluxCreateSecretOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxCreateSecretOptions.Generated.cs @@ -183,4 +183,10 @@ public record FluxCreateSecretOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxCreateSourceGitOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxCreateSourceGitOptions.Generated.cs index 7f016bbca86..8c1d4168ba7 100644 --- a/src/ModularPipelines.Flux/Options/FluxCreateSourceGitOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxCreateSourceGitOptions.Generated.cs @@ -61,6 +61,7 @@ public record FluxCreateSourceGitOptions : FluxOptions /// /// path to a passwordless private key file used for authenticating to the Git SSH server /// + [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxCreateSourceOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxCreateSourceOptions.Generated.cs index 4fc82b2d76b..c6d35a75ea2 100644 --- a/src/ModularPipelines.Flux/Options/FluxCreateSourceOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxCreateSourceOptions.Generated.cs @@ -189,4 +189,10 @@ public record FluxCreateSourceOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxDebugOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxDebugOptions.Generated.cs index c7b8b26cc8d..118b8abfa19 100644 --- a/src/ModularPipelines.Flux/Options/FluxDebugOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxDebugOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxDebugOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxDeleteImageOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxDeleteImageOptions.Generated.cs index e46a536518a..f27159598ec 100644 --- a/src/ModularPipelines.Flux/Options/FluxDeleteImageOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxDeleteImageOptions.Generated.cs @@ -171,4 +171,10 @@ public record FluxDeleteImageOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxDeleteOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxDeleteOptions.Generated.cs index 6c8a732a9db..98ec8fc365a 100644 --- a/src/ModularPipelines.Flux/Options/FluxDeleteOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxDeleteOptions.Generated.cs @@ -171,4 +171,10 @@ public record FluxDeleteOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxDeleteSourceOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxDeleteSourceOptions.Generated.cs index eae44beb014..b800fda1fb4 100644 --- a/src/ModularPipelines.Flux/Options/FluxDeleteSourceOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxDeleteSourceOptions.Generated.cs @@ -171,4 +171,10 @@ public record FluxDeleteSourceOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxDiffOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxDiffOptions.Generated.cs index ca97ebe35e2..68ca2dd1c0b 100644 --- a/src/ModularPipelines.Flux/Options/FluxDiffOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxDiffOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxDiffOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxExportArtifactOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxExportArtifactOptions.Generated.cs index 083b93322c1..43c6720a1b2 100644 --- a/src/ModularPipelines.Flux/Options/FluxExportArtifactOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxExportArtifactOptions.Generated.cs @@ -171,4 +171,10 @@ public record FluxExportArtifactOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxExportImageOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxExportImageOptions.Generated.cs index b772f04f5ac..748c05cb58e 100644 --- a/src/ModularPipelines.Flux/Options/FluxExportImageOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxExportImageOptions.Generated.cs @@ -171,4 +171,10 @@ public record FluxExportImageOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxExportOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxExportOptions.Generated.cs index 60c21870932..76e03f9cfd5 100644 --- a/src/ModularPipelines.Flux/Options/FluxExportOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxExportOptions.Generated.cs @@ -171,4 +171,10 @@ public record FluxExportOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxExportSourceOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxExportSourceOptions.Generated.cs index 9579817b7c0..d2cfa585df6 100644 --- a/src/ModularPipelines.Flux/Options/FluxExportSourceOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxExportSourceOptions.Generated.cs @@ -177,4 +177,10 @@ public record FluxExportSourceOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxGetArtifactsOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxGetArtifactsOptions.Generated.cs index ab4cab7923f..6c8091219fe 100644 --- a/src/ModularPipelines.Flux/Options/FluxGetArtifactsOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxGetArtifactsOptions.Generated.cs @@ -195,4 +195,10 @@ public record FluxGetArtifactsOptions : FluxOptions [CliFlag("--watch", ShortForm = "-w")] public bool? Watch { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxGetImagesOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxGetImagesOptions.Generated.cs index ab8c4233d1b..dc8dbbb26b2 100644 --- a/src/ModularPipelines.Flux/Options/FluxGetImagesOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxGetImagesOptions.Generated.cs @@ -195,4 +195,10 @@ public record FluxGetImagesOptions : FluxOptions [CliFlag("--watch", ShortForm = "-w")] public bool? Watch { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxGetOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxGetOptions.Generated.cs index f749ddef1d0..ba7df69416c 100644 --- a/src/ModularPipelines.Flux/Options/FluxGetOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxGetOptions.Generated.cs @@ -195,4 +195,10 @@ public record FluxGetOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxGetSourcesOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxGetSourcesOptions.Generated.cs index 8ff46362cac..471c593c0f1 100644 --- a/src/ModularPipelines.Flux/Options/FluxGetSourcesOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxGetSourcesOptions.Generated.cs @@ -195,4 +195,10 @@ public record FluxGetSourcesOptions : FluxOptions [CliFlag("--watch", ShortForm = "-w")] public bool? Watch { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxListOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxListOptions.Generated.cs index 5cf7e15052b..b69c06fc6e3 100644 --- a/src/ModularPipelines.Flux/Options/FluxListOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxListOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxListOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxPluginInstallOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxPluginInstallOptions.Generated.cs index 949498f29d2..7499b0c760a 100644 --- a/src/ModularPipelines.Flux/Options/FluxPluginInstallOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxPluginInstallOptions.Generated.cs @@ -19,7 +19,7 @@ namespace ModularPipelines.Flux.Options; [ExcludeFromCodeCoverage] [CliSubCommand("plugin", "install")] public record FluxPluginInstallOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string NameVersion ) : FluxOptions { /// diff --git a/src/ModularPipelines.Flux/Options/FluxPluginOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxPluginOptions.Generated.cs index bb53c85a3e5..eacfb1fd1c2 100644 --- a/src/ModularPipelines.Flux/Options/FluxPluginOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxPluginOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxPluginOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxPullOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxPullOptions.Generated.cs index 01fa4888c18..49cf7ffbcbd 100644 --- a/src/ModularPipelines.Flux/Options/FluxPullOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxPullOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxPullOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxPushOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxPushOptions.Generated.cs index 90bc0a8a594..8667d658752 100644 --- a/src/ModularPipelines.Flux/Options/FluxPushOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxPushOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxPushOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxReconcileImageOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxReconcileImageOptions.Generated.cs index 9d0cec81069..ffb6ef2f883 100644 --- a/src/ModularPipelines.Flux/Options/FluxReconcileImageOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxReconcileImageOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxReconcileImageOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxReconcileOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxReconcileOptions.Generated.cs index e9e281cf255..741d462711f 100644 --- a/src/ModularPipelines.Flux/Options/FluxReconcileOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxReconcileOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxReconcileOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxReconcileSourceOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxReconcileSourceOptions.Generated.cs index 9f98df24a3e..c28052de772 100644 --- a/src/ModularPipelines.Flux/Options/FluxReconcileSourceOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxReconcileSourceOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxReconcileSourceOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxResumeImageOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxResumeImageOptions.Generated.cs index 9f27b436ed9..70cbc2a06f0 100644 --- a/src/ModularPipelines.Flux/Options/FluxResumeImageOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxResumeImageOptions.Generated.cs @@ -177,4 +177,10 @@ public record FluxResumeImageOptions : FluxOptions [CliFlag("--wait")] public bool? Wait { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxResumeOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxResumeOptions.Generated.cs index 5ba419ea60d..ab3a8066faa 100644 --- a/src/ModularPipelines.Flux/Options/FluxResumeOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxResumeOptions.Generated.cs @@ -177,4 +177,10 @@ public record FluxResumeOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxResumeSourceOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxResumeSourceOptions.Generated.cs index ba72e15b78e..d7c7b91d2b9 100644 --- a/src/ModularPipelines.Flux/Options/FluxResumeSourceOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxResumeSourceOptions.Generated.cs @@ -177,4 +177,10 @@ public record FluxResumeSourceOptions : FluxOptions [CliFlag("--wait")] public bool? Wait { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxSuspendImageOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxSuspendImageOptions.Generated.cs index 0f5cc0de93b..6990d81b5a5 100644 --- a/src/ModularPipelines.Flux/Options/FluxSuspendImageOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxSuspendImageOptions.Generated.cs @@ -171,4 +171,10 @@ public record FluxSuspendImageOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxSuspendOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxSuspendOptions.Generated.cs index b9bf75963c9..80bc09d9539 100644 --- a/src/ModularPipelines.Flux/Options/FluxSuspendOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxSuspendOptions.Generated.cs @@ -171,4 +171,10 @@ public record FluxSuspendOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxSuspendSourceOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxSuspendSourceOptions.Generated.cs index ff13cf5cd26..8edf4122cf0 100644 --- a/src/ModularPipelines.Flux/Options/FluxSuspendSourceOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxSuspendSourceOptions.Generated.cs @@ -171,4 +171,10 @@ public record FluxSuspendSourceOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxTagOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxTagOptions.Generated.cs index c6c448eb761..332fffb8164 100644 --- a/src/ModularPipelines.Flux/Options/FluxTagOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxTagOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxTagOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxTreeArtifactOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxTreeArtifactOptions.Generated.cs index 9385b791ea5..cfe8126ea8a 100644 --- a/src/ModularPipelines.Flux/Options/FluxTreeArtifactOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxTreeArtifactOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxTreeArtifactOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxTreeOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxTreeOptions.Generated.cs index 5056b9ce8c9..62f5ab9eb9d 100644 --- a/src/ModularPipelines.Flux/Options/FluxTreeOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxTreeOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxTreeOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Flux/Options/FluxTriggerOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxTriggerOptions.Generated.cs index da562a3621c..e3a39132a8e 100644 --- a/src/ModularPipelines.Flux/Options/FluxTriggerOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxTriggerOptions.Generated.cs @@ -165,4 +165,10 @@ public record FluxTriggerOptions : FluxOptions [CliFlag("--verbose")] public bool? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Grype/Options/GrypeDbOptions.Generated.cs b/src/ModularPipelines.Grype/Options/GrypeDbOptions.Generated.cs index 263f4ac7667..aa715a4faef 100644 --- a/src/ModularPipelines.Grype/Options/GrypeDbOptions.Generated.cs +++ b/src/ModularPipelines.Grype/Options/GrypeDbOptions.Generated.cs @@ -50,4 +50,10 @@ public record GrypeDbOptions : GrypeOptions [CliOption("--verbose", ShortForm = "-v", Format = OptionFormat.EqualsSeparated)] public int? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Grype/Options/GrypeDbSearchOptions.Generated.cs b/src/ModularPipelines.Grype/Options/GrypeDbSearchOptions.Generated.cs index 74e0d628627..e0a0a430762 100644 --- a/src/ModularPipelines.Grype/Options/GrypeDbSearchOptions.Generated.cs +++ b/src/ModularPipelines.Grype/Options/GrypeDbSearchOptions.Generated.cs @@ -116,4 +116,10 @@ public record GrypeDbSearchOptions : GrypeOptions [CliOption("--verbose", ShortForm = "-v", Format = OptionFormat.EqualsSeparated)] public int? Verbose { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Kubernetes/Extensions/KustomizeExtensions.Generated.cs b/src/ModularPipelines.Kubernetes/Extensions/KustomizeExtensions.Generated.cs index d16fb481081..953fa1e9824 100644 --- a/src/ModularPipelines.Kubernetes/Extensions/KustomizeExtensions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Extensions/KustomizeExtensions.Generated.cs @@ -36,7 +36,7 @@ public static IServiceCollection RegisterKustomizeContext(this IServiceCollectio } /// - /// Gets the kustomize service from the pipeline context for compatibility. + /// Gets the kustomize service from the pipeline context. /// /// The pipeline context. /// The service for executing kustomize commands. diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCatOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCatOptions.Generated.cs index e1c34d5b5b7..961330a969a 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCatOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCatOptions.Generated.cs @@ -18,9 +18,7 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("cfg", "cat")] -public record KustomizeCfgCatOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Dir -) : KustomizeOptions +public record KustomizeCfgCatOptions : KustomizeOptions { /// /// annotate resources with their file origins. diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCountOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCountOptions.Generated.cs index 1e45b60b304..0fa7899360d 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCountOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCountOptions.Generated.cs @@ -44,9 +44,6 @@ public record KustomizeCfgCountOptions : KustomizeOptions [CliFlag("--stack-trace")] public bool? StackTrace { get; set; } - /// - /// The DIR operand. - /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] public string? Dir { get; set; } diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgGrepOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgGrepOptions.Generated.cs index 8d2f823ca94..2fc15e404bc 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgGrepOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgGrepOptions.Generated.cs @@ -18,9 +18,7 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("cfg", "grep")] -public record KustomizeCfgGrepOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Query -) : KustomizeOptions +public record KustomizeCfgGrepOptions : KustomizeOptions { /// /// annotate resources with their file origins. (default true) @@ -52,10 +50,7 @@ public record KustomizeCfgGrepOptions( [CliFlag("--stack-trace")] public bool? StackTrace { get; set; } - /// - /// The DIR operand. - /// - [CliArgument(1, Phase = CommandLinePhase.EarlyOperand)] + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] public string? Dir { get; set; } } diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgTreeOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgTreeOptions.Generated.cs index a7abe41e911..e76b51595ba 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgTreeOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgTreeOptions.Generated.cs @@ -110,9 +110,6 @@ public record KustomizeCfgTreeOptions : KustomizeOptions [CliFlag("--stack-trace")] public bool? StackTrace { get; set; } - /// - /// The DIR operand. - /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] public string? Dir { get; set; } diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeCreateOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeCreateOptions.Generated.cs index 8ba62b4f394..4bd9891d29b 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeCreateOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeCreateOptions.Generated.cs @@ -24,7 +24,7 @@ public record KustomizeCreateOptions : KustomizeOptions /// Add one or more common annotations. /// [CliOption("--annotations", Format = OptionFormat.EqualsSeparated)] - public IEnumerable? Annotations { get; set; } + public string? Annotations { get; set; } /// /// Search for kubernetes resources in the current directory to be added to the kustomization file. @@ -42,7 +42,7 @@ public record KustomizeCreateOptions : KustomizeOptions /// Add one or more common labels. /// [CliOption("--labels", Format = OptionFormat.EqualsSeparated)] - public IEnumerable? Labels { get; set; } + public string? Labels { get; set; } /// /// Sets the value of the namePrefix field in the kustomization file. diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddBuildmetadataOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddBuildmetadataOptions.Generated.cs index bb1c98f9906..0f16d610867 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddBuildmetadataOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddBuildmetadataOptions.Generated.cs @@ -18,9 +18,7 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "add", "buildmetadata")] -public record KustomizeEditAddBuildmetadataOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Metadata -) : KustomizeOptions +public record KustomizeEditAddBuildmetadataOptions : KustomizeOptions { /// /// help for buildmetadata diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddConfigmapOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddConfigmapOptions.Generated.cs index b3cfa61083a..fe6499370d5 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddConfigmapOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddConfigmapOptions.Generated.cs @@ -18,9 +18,7 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "add", "configmap")] -public record KustomizeEditAddConfigmapOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name -) : KustomizeOptions +public record KustomizeEditAddConfigmapOptions : KustomizeOptions { /// /// Specify the behavior for config map generation, i.e whether to create a new configmap (the default), to merge with a previously defined one, or to replace an existing one. Merge and replace should be used only when overriding an existing configmap defined in a base @@ -32,7 +30,7 @@ public record KustomizeEditAddConfigmapOptions( /// Disable the name suffix for the configmap /// [CliFlag("--disableNameSuffixHash")] - public bool? DisableNameSuffixHash { get; set; } + public bool? Disablenamesuffixhash { get; set; } /// /// Specify the path to a file to read lines of key=val pairs to create a configmap (i.e. a Docker .env file). diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddSecretOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddSecretOptions.Generated.cs index ca6c4392c63..71042c2be0d 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddSecretOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddSecretOptions.Generated.cs @@ -18,15 +18,13 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "add", "secret")] -public record KustomizeEditAddSecretOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name -) : KustomizeOptions +public record KustomizeEditAddSecretOptions : KustomizeOptions { /// /// Disable the name suffix for the secret /// [CliFlag("--disableNameSuffixHash")] - public bool? DisableNameSuffixHash { get; set; } + public bool? Disablenamesuffixhash { get; set; } /// /// Specify the path to a file to read lines of key=val pairs to create a secret (i.e. a Docker .env file). diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveBuildmetadataOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveBuildmetadataOptions.Generated.cs index 65bc499db19..50f99feb0a5 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveBuildmetadataOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveBuildmetadataOptions.Generated.cs @@ -18,9 +18,7 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "remove", "buildmetadata")] -public record KustomizeEditRemoveBuildmetadataOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Metadata -) : KustomizeOptions +public record KustomizeEditRemoveBuildmetadataOptions : KustomizeOptions { /// /// help for buildmetadata diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveConfigmapOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveConfigmapOptions.Generated.cs index a1ae4d6b6d1..7e9b10d523d 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveConfigmapOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveConfigmapOptions.Generated.cs @@ -18,9 +18,7 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "remove", "configmap")] -public record KustomizeEditRemoveConfigmapOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name -) : KustomizeOptions +public record KustomizeEditRemoveConfigmapOptions : KustomizeOptions { /// /// help for configmap diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveSecretOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveSecretOptions.Generated.cs index b458f32b30f..8ea6c123297 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveSecretOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveSecretOptions.Generated.cs @@ -18,9 +18,7 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "remove", "secret")] -public record KustomizeEditRemoveSecretOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name -) : KustomizeOptions +public record KustomizeEditRemoveSecretOptions : KustomizeOptions { /// /// help for secret diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetBuildmetadataOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetBuildmetadataOptions.Generated.cs index f05c2fb6435..6c32d372d1c 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetBuildmetadataOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetBuildmetadataOptions.Generated.cs @@ -18,9 +18,7 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "set", "buildmetadata")] -public record KustomizeEditSetBuildmetadataOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Metadata -) : KustomizeOptions +public record KustomizeEditSetBuildmetadataOptions : KustomizeOptions { /// /// help for buildmetadata diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetConfigmapOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetConfigmapOptions.Generated.cs index 5d9fafbfb95..71eda5e8ad9 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetConfigmapOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetConfigmapOptions.Generated.cs @@ -18,9 +18,7 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "set", "configmap")] -public record KustomizeEditSetConfigmapOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name -) : KustomizeOptions +public record KustomizeEditSetConfigmapOptions : KustomizeOptions { /// /// Specify an existing key and a new value to update a ConfigMap (i.e. mykey=newvalue) diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetSecretOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetSecretOptions.Generated.cs index 733da974ede..ff3167c1e32 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetSecretOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetSecretOptions.Generated.cs @@ -18,9 +18,7 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "set", "secret")] -public record KustomizeEditSetSecretOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name -) : KustomizeOptions +public record KustomizeEditSetSecretOptions : KustomizeOptions { /// /// Specify an existing key and a new value to update a Secret (i.e. mykey=newvalue) diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeFnRunOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeFnRunOptions.Generated.cs index 543a39f2902..800a6418555 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeFnRunOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeFnRunOptions.Generated.cs @@ -110,9 +110,6 @@ public record KustomizeFnRunOptions : KustomizeOptions [CliFlag("--stack-trace")] public bool? StackTrace { get; set; } - /// - /// The DIR operand. - /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] public string? Dir { get; set; } diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeLocalizeOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeLocalizeOptions.Generated.cs index 8ae89a58818..610aeaa2d3c 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeLocalizeOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeLocalizeOptions.Generated.cs @@ -44,16 +44,4 @@ public record KustomizeLocalizeOptions : KustomizeOptions [CliFlag("--stack-trace")] public bool? StackTrace { get; set; } - /// - /// The target operand. - /// - [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public string? Target { get; set; } - - /// - /// The destination operand. - /// - [CliArgument(1, Phase = CommandLinePhase.EarlyOperand)] - public string? Destination { get; set; } - } diff --git a/src/ModularPipelines.Kubernetes/Services/IKustomizeCfg.Generated.cs b/src/ModularPipelines.Kubernetes/Services/IKustomizeCfg.Generated.cs index 8aa6299c3b5..0948a61def2 100644 --- a/src/ModularPipelines.Kubernetes/Services/IKustomizeCfg.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/IKustomizeCfg.Generated.cs @@ -15,9 +15,6 @@ namespace ModularPipelines.Kubernetes.Services; /// /// kustomize cfg commands. /// -/// -/// Nested sub-command groups are exposed as concrete services; only this top-level facade is interface-backed. -/// [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] public interface IKustomizeCfg { @@ -38,7 +35,7 @@ Task ExecuteAsync(KustomizeCfgOptions? options = null, CommandExe /// The execution configuration options. /// Cancellation token. /// The command result. - Task CatAsync(KustomizeCfgCatOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) + Task CatAsync(KustomizeCfgCatOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) => throw new System.NotSupportedException(); /// @@ -58,7 +55,7 @@ Task CountAsync(KustomizeCfgCountOptions? options = null, Command /// The execution configuration options. /// Cancellation token. /// The command result. - Task GrepAsync(KustomizeCfgGrepOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) + Task GrepAsync(KustomizeCfgGrepOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) => throw new System.NotSupportedException(); /// diff --git a/src/ModularPipelines.Kubernetes/Services/IKustomizeEdit.Generated.cs b/src/ModularPipelines.Kubernetes/Services/IKustomizeEdit.Generated.cs index f608755445a..48dc6d14be8 100644 --- a/src/ModularPipelines.Kubernetes/Services/IKustomizeEdit.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/IKustomizeEdit.Generated.cs @@ -15,9 +15,6 @@ namespace ModularPipelines.Kubernetes.Services; /// /// kustomize edit commands. /// -/// -/// Nested sub-command groups are exposed as concrete services; only this top-level facade is interface-backed. -/// [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] public interface IKustomizeEdit { diff --git a/src/ModularPipelines.Kubernetes/Services/IKustomizeFn.Generated.cs b/src/ModularPipelines.Kubernetes/Services/IKustomizeFn.Generated.cs index 63ee52e5a9c..3b4a2c4f411 100644 --- a/src/ModularPipelines.Kubernetes/Services/IKustomizeFn.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/IKustomizeFn.Generated.cs @@ -15,9 +15,6 @@ namespace ModularPipelines.Kubernetes.Services; /// /// kustomize fn commands. /// -/// -/// Nested sub-command groups are exposed as concrete services; only this top-level facade is interface-backed. -/// [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] public interface IKustomizeFn { diff --git a/src/ModularPipelines.Kubernetes/Services/KustomizeCfg.Generated.cs b/src/ModularPipelines.Kubernetes/Services/KustomizeCfg.Generated.cs index 3bd92ade885..6ffc8d66972 100644 --- a/src/ModularPipelines.Kubernetes/Services/KustomizeCfg.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/KustomizeCfg.Generated.cs @@ -54,11 +54,11 @@ public virtual async Task ExecuteAsync( /// Cancellation token. /// The command result. public virtual async Task CatAsync( - KustomizeCfgCatOptions options, + KustomizeCfgCatOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeCfgCatOptions(), executionOptions, cancellationToken); } /// @@ -84,11 +84,11 @@ public virtual async Task CountAsync( /// Cancellation token. /// The command result. public virtual async Task GrepAsync( - KustomizeCfgGrepOptions options, + KustomizeCfgGrepOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeCfgGrepOptions(), executionOptions, cancellationToken); } /// diff --git a/src/ModularPipelines.Kubernetes/Services/KustomizeEditAdd.Generated.cs b/src/ModularPipelines.Kubernetes/Services/KustomizeEditAdd.Generated.cs index 7f073493880..928b77a5e27 100644 --- a/src/ModularPipelines.Kubernetes/Services/KustomizeEditAdd.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/KustomizeEditAdd.Generated.cs @@ -31,21 +31,6 @@ public KustomizeEditAdd(ICommandContext command) #region Commands - /// - /// Adds an item to the kustomization file - /// - /// The command options. - /// The execution configuration options. - /// Cancellation token. - /// The command result. - public virtual async Task ExecuteAsync( - KustomizeEditAddOptions? options = null, - CommandExecutionOptions? executionOptions = null, - CancellationToken cancellationToken = default) - { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditAddOptions(), executionOptions, cancellationToken); - } - /// /// Adds one or more commonAnnotations to kustomization.yaml /// @@ -84,11 +69,11 @@ public virtual async Task BaseAsync( /// Cancellation token. /// The command result. public virtual async Task BuildmetadataAsync( - KustomizeEditAddBuildmetadataOptions options, + KustomizeEditAddBuildmetadataOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditAddBuildmetadataOptions(), executionOptions, cancellationToken); } /// @@ -114,11 +99,11 @@ public virtual async Task ComponentAsync( /// Cancellation token. /// The command result. public virtual async Task ConfigmapAsync( - KustomizeEditAddConfigmapOptions options, + KustomizeEditAddConfigmapOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditAddConfigmapOptions(), executionOptions, cancellationToken); } /// @@ -189,11 +174,11 @@ public virtual async Task ResourceAsync( /// Cancellation token. /// The command result. public virtual async Task SecretAsync( - KustomizeEditAddSecretOptions options, + KustomizeEditAddSecretOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditAddSecretOptions(), executionOptions, cancellationToken); } /// diff --git a/src/ModularPipelines.Kubernetes/Services/KustomizeEditRemove.Generated.cs b/src/ModularPipelines.Kubernetes/Services/KustomizeEditRemove.Generated.cs index b905d35db0f..1e3f92e735b 100644 --- a/src/ModularPipelines.Kubernetes/Services/KustomizeEditRemove.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/KustomizeEditRemove.Generated.cs @@ -31,21 +31,6 @@ public KustomizeEditRemove(ICommandContext command) #region Commands - /// - /// Removes items from the kustomization file - /// - /// The command options. - /// The execution configuration options. - /// Cancellation token. - /// The command result. - public virtual async Task ExecuteAsync( - KustomizeEditRemoveOptions? options = null, - CommandExecutionOptions? executionOptions = null, - CancellationToken cancellationToken = default) - { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditRemoveOptions(), executionOptions, cancellationToken); - } - /// /// Removes one or more commonAnnotations from kustomization.yaml /// @@ -69,11 +54,11 @@ public virtual async Task AnnotationAsync( /// Cancellation token. /// The command result. public virtual async Task BuildmetadataAsync( - KustomizeEditRemoveBuildmetadataOptions options, + KustomizeEditRemoveBuildmetadataOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditRemoveBuildmetadataOptions(), executionOptions, cancellationToken); } /// @@ -84,11 +69,11 @@ public virtual async Task BuildmetadataAsync( /// Cancellation token. /// The command result. public virtual async Task ConfigmapAsync( - KustomizeEditRemoveConfigmapOptions options, + KustomizeEditRemoveConfigmapOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditRemoveConfigmapOptions(), executionOptions, cancellationToken); } /// @@ -144,11 +129,11 @@ public virtual async Task ResourceAsync( /// Cancellation token. /// The command result. public virtual async Task SecretAsync( - KustomizeEditRemoveSecretOptions options, + KustomizeEditRemoveSecretOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditRemoveSecretOptions(), executionOptions, cancellationToken); } /// diff --git a/src/ModularPipelines.Kubernetes/Services/KustomizeEditSet.Generated.cs b/src/ModularPipelines.Kubernetes/Services/KustomizeEditSet.Generated.cs index abe46f89af0..faf6c60dcfa 100644 --- a/src/ModularPipelines.Kubernetes/Services/KustomizeEditSet.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/KustomizeEditSet.Generated.cs @@ -31,21 +31,6 @@ public KustomizeEditSet(ICommandContext command) #region Commands - /// - /// Sets the value of different fields in kustomization file - /// - /// The command options. - /// The execution configuration options. - /// Cancellation token. - /// The command result. - public virtual async Task ExecuteAsync( - KustomizeEditSetOptions? options = null, - CommandExecutionOptions? executionOptions = null, - CancellationToken cancellationToken = default) - { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditSetOptions(), executionOptions, cancellationToken); - } - /// /// Sets one or more commonAnnotations in kustomization.yaml /// @@ -69,11 +54,11 @@ public virtual async Task AnnotationAsync( /// Cancellation token. /// The command result. public virtual async Task BuildmetadataAsync( - KustomizeEditSetBuildmetadataOptions options, + KustomizeEditSetBuildmetadataOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditSetBuildmetadataOptions(), executionOptions, cancellationToken); } /// @@ -84,11 +69,11 @@ public virtual async Task BuildmetadataAsync( /// Cancellation token. /// The command result. public virtual async Task ConfigmapAsync( - KustomizeEditSetConfigmapOptions options, + KustomizeEditSetConfigmapOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditSetConfigmapOptions(), executionOptions, cancellationToken); } /// @@ -189,11 +174,11 @@ public virtual async Task ReplicasAsync( /// Cancellation token. /// The command result. public virtual async Task SecretAsync( - KustomizeEditSetSecretOptions options, + KustomizeEditSetSecretOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditSetSecretOptions(), executionOptions, cancellationToken); } #endregion diff --git a/src/ModularPipelines.Pulumi/Generated/Pulumi.CommandCoverage.json b/src/ModularPipelines.Pulumi/Generated/Pulumi.CommandCoverage.json index ff837da322a..f0561157791 100644 --- a/src/ModularPipelines.Pulumi/Generated/Pulumi.CommandCoverage.json +++ b/src/ModularPipelines.Pulumi/Generated/Pulumi.CommandCoverage.json @@ -1,7 +1,7 @@ { "formatVersion": 1, "toolName": "pulumi", - "toolVersion": "v3.256.0", + "toolVersion": "v3.255.0", "commandCount": 226, "commandTreeSha256": "c23fbb83a37eb69c9cf574d6e85ffad67be77a27cab19c1f4a05c22b31237a9e", "commands": [ diff --git a/src/ModularPipelines.Pulumi/Options/PulumiDeploymentRunOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiDeploymentRunOptions.Generated.cs index e34cc0ca75e..57cdb774312 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiDeploymentRunOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiDeploymentRunOptions.Generated.cs @@ -71,6 +71,7 @@ public record PulumiDeploymentRunOptions( /// /// Git SSH private key path; use --git-auth-password for the password, if needed /// + [SecretValue] [CliOption("--git-auth-ssh-private-key-path", Format = OptionFormat.EqualsSeparated)] public string? GitAuthSshPrivateKeyPath { get; set; } diff --git a/src/ModularPipelines.Pulumi/Options/PulumiDestroyOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiDestroyOptions.Generated.cs index 42144e71142..da50fa574bb 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiDestroyOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiDestroyOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; -using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -75,12 +74,6 @@ public record PulumiDestroyOptions : PulumiOptions [CliFlag("--help", ShortForm = "-h")] public bool? Help { get; set; } - /// - /// Ignore the protect resource option for this operation, allowing protected resources to be destroyed. Use with caution: deleted resources cannot be recovered - /// - [CliFlag("--ignore-protect")] - public bool? IgnoreProtect { get; set; } - /// /// Serialize the destroy diffs, operations, and overall output as JSON /// @@ -121,7 +114,7 @@ public record PulumiDestroyOptions : PulumiOptions /// Refresh the state of the stack's resources before this update /// [CliOption("--refresh", ShortForm = "-r", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? Refresh { get; set; } + public string? Refresh { get; set; } /// /// Remove the stack and its config file after all resources in the stack have been deleted @@ -193,7 +186,7 @@ public record PulumiDestroyOptions : PulumiOptions /// Suppress display of the state permalink /// [CliOption("--suppress-permalink", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? SuppressPermalink { get; set; } + public string? SuppressPermalink { get; set; } /// /// Suppress display of periodic progress dots diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvDiffOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvDiffOptions.Generated.cs index 65b12be8a53..4e918a044e7 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvDiffOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvDiffOptions.Generated.cs @@ -19,8 +19,7 @@ namespace ModularPipelines.Pulumi.Options; [ExcludeFromCodeCoverage] [CliSubCommand("env", "diff")] public record PulumiEnvDiffOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string EnvironmentName, - [property: CliArgument(1, Phase = CommandLinePhase.EarlyOperand, Required = true)] string OrgNameOrProjectNameOrEnvironmentNameVersion + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string EnvironmentName ) : PulumiOptions { /// @@ -131,4 +130,10 @@ public record PulumiEnvDiffOptions( [CliOption("--verbose", ShortForm = "-v", Format = OptionFormat.EqualsSeparated)] public int? Verbose { get; set; } + /// + /// The [[org-name Or ][<project-name> Or ]<environment-name>]@<version> operand. + /// + [CliArgument(1, Phase = CommandLinePhase.EarlyOperand)] + public string? OrgNameOrProjectNameOrEnvironmentNameVersion { get; set; } + } diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvEditOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvEditOptions.Generated.cs index f2ec554e248..39921251013 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvEditOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvEditOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; -using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -27,7 +26,7 @@ public record PulumiEnvEditOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? Draft { get; set; } + public string? Draft { get; set; } /// /// the command to use to edit the environment definition diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginOidcOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginOidcOptions.Generated.cs index 938a84ef1cf..bcc0787b71f 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginOidcOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginOidcOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; -using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -35,7 +34,7 @@ public record PulumiEnvProviderAwsLoginOidcOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? Draft { get; set; } + public string? Draft { get; set; } /// /// optional session duration, e.g. 1h @@ -43,12 +42,6 @@ public record PulumiEnvProviderAwsLoginOidcOptions( [CliOption("--duration", Format = OptionFormat.EqualsSeparated)] public string? Duration { get; set; } - /// - /// also set the AWS SDK environment variables (AWS_ACCESS_KEY_ID, etc.) referencing the login outputs - /// - [CliFlag("--export-env-vars")] - public bool? ExportEnvVars { get; set; } - /// /// help for oidc /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginStaticOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginStaticOptions.Generated.cs index e35041f1e9f..a77694c1d6d 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginStaticOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginStaticOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; -using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -35,13 +34,7 @@ public record PulumiEnvProviderAwsLoginStaticOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? Draft { get; set; } - - /// - /// also set the AWS SDK environment variables (AWS_ACCESS_KEY_ID, etc.) referencing the login outputs - /// - [CliFlag("--export-env-vars")] - public bool? ExportEnvVars { get; set; } + public string? Draft { get; set; } /// /// help for static diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginOidcOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginOidcOptions.Generated.cs index c10e899759e..7a7fc0934d6 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginOidcOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginOidcOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; -using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -36,13 +35,7 @@ public record PulumiEnvProviderAzureLoginOidcOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? Draft { get; set; } - - /// - /// also set the Azure environment variables (ARM_CLIENT_ID, etc.) referencing the login outputs - /// - [CliFlag("--export-env-vars")] - public bool? ExportEnvVars { get; set; } + public string? Draft { get; set; } /// /// help for oidc diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginStaticOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginStaticOptions.Generated.cs index eb7cfc919b2..dac3783ebef 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginStaticOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginStaticOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; -using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -37,13 +36,7 @@ public record PulumiEnvProviderAzureLoginStaticOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? Draft { get; set; } - - /// - /// also set the Azure environment variables (ARM_CLIENT_ID, etc.) referencing the login outputs - /// - [CliFlag("--export-env-vars")] - public bool? ExportEnvVars { get; set; } + public string? Draft { get; set; } /// /// help for static diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginOidcOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginOidcOptions.Generated.cs index 835e2e5cb5c..ad9223ddc7c 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginOidcOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginOidcOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; -using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -34,13 +33,7 @@ public record PulumiEnvProviderGcpLoginOidcOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? Draft { get; set; } - - /// - /// also set the Google SDK environment variables (GOOGLE_PROJECT, etc.) referencing the login outputs - /// - [CliFlag("--export-env-vars")] - public bool? ExportEnvVars { get; set; } + public string? Draft { get; set; } /// /// help for oidc diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginStaticOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginStaticOptions.Generated.cs index 8cd3b484cdd..fd6621de3f2 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginStaticOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginStaticOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; -using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -35,13 +34,7 @@ public record PulumiEnvProviderGcpLoginStaticOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? Draft { get; set; } - - /// - /// also set the Google SDK environment variables (GOOGLE_PROJECT, etc.) referencing the login outputs - /// - [CliFlag("--export-env-vars")] - public bool? ExportEnvVars { get; set; } + public string? Draft { get; set; } /// /// help for static diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvSetOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvSetOptions.Generated.cs index d7e0972c591..9ebdd4eefaa 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvSetOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvSetOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; -using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -29,7 +28,7 @@ public record PulumiEnvSetOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? Draft { get; set; } + public string? Draft { get; set; } /// /// If set, the value is read from the specified file. Pass - to read from standard input. diff --git a/src/ModularPipelines.Pulumi/Options/PulumiImportOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiImportOptions.Generated.cs index 42b2124e296..9317765bdd0 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiImportOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiImportOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; -using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -151,7 +150,7 @@ public record PulumiImportOptions : PulumiOptions /// Suppress display of the state permalink /// [CliOption("--suppress-permalink", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? SuppressPermalink { get; set; } + public string? SuppressPermalink { get; set; } /// /// Suppress display of periodic progress dots diff --git a/src/ModularPipelines.Pulumi/Options/PulumiNewOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiNewOptions.Generated.cs index 2f363f2dbd2..317a25ab769 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiNewOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiNewOptions.Generated.cs @@ -20,6 +20,12 @@ namespace ModularPipelines.Pulumi.Options; [CliSubCommand("new")] public record PulumiNewOptions : PulumiOptions { + /// + /// Prompt to use for Pulumi AI + /// + [CliOption("--ai", Format = OptionFormat.EqualsSeparated)] + public string? Ai { get; set; } + /// /// Config to save /// @@ -62,6 +68,12 @@ public record PulumiNewOptions : PulumiOptions [CliFlag("--help", ShortForm = "-h")] public bool? Help { get; set; } + /// + /// Language to use for Pulumi AI (must be one of TypeScript, JavaScript, Python, Go, C#, Java, or YAML) + /// + [CliOption("--language", Format = OptionFormat.EqualsSeparated)] + public string? Language { get; set; } + /// /// List locally installed templates and exit /// @@ -98,6 +110,12 @@ public record PulumiNewOptions : PulumiOptions [CliOption("--stack", ShortForm = "-s", Format = OptionFormat.EqualsSeparated)] public string? Stack { get; set; } + /// + /// Run in template mode, which will skip prompting for AI or Template functionality + /// + [CliFlag("--template-mode", ShortForm = "-t")] + public bool? TemplateMode { get; set; } + /// /// Skip prompts and proceed with default values /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPackageAddOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPackageAddOptions.Generated.cs index b0e4fa39836..54e25872d80 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPackageAddOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPackageAddOptions.Generated.cs @@ -40,12 +40,6 @@ public record PulumiPackageAddOptions( [CliOption("--language", Format = OptionFormat.EqualsSeparated)] public string? Language { get; set; } - /// - /// A URL to download the plugin from. When set, the provider argument is used as the plugin name directly and no package resolution is performed. - /// - [CliOption("--server", Format = OptionFormat.EqualsSeparated)] - public string? Server { get; set; } - /// /// Colorize output. Choices are: always, never, raw, auto (default "auto") /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPackageGenSdkOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPackageGenSdkOptions.Generated.cs index b07430e7570..1b39cef8f71 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPackageGenSdkOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPackageGenSdkOptions.Generated.cs @@ -52,12 +52,6 @@ public record PulumiPackageGenSdkOptions( [CliOption("--out", ShortForm = "-o", Format = OptionFormat.EqualsSeparated)] public string? Out { get; set; } - /// - /// A URL to download the plugin from. When set, the provider argument is used as the plugin name directly and no package resolution is performed. - /// - [CliOption("--server", Format = OptionFormat.EqualsSeparated)] - public string? Server { get; set; } - /// /// The provider plugin version to generate the SDK for /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPackageGetMappingOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPackageGetMappingOptions.Generated.cs index a1efc6f2767..bccae586c92 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPackageGetMappingOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPackageGetMappingOptions.Generated.cs @@ -35,12 +35,6 @@ public record PulumiPackageGetMappingOptions( [CliOption("--out", ShortForm = "-o", Format = OptionFormat.EqualsSeparated)] public string? Out { get; set; } - /// - /// A URL to download the plugin from. When set, the provider argument is used as the plugin name directly and no package resolution is performed. - /// - [CliOption("--server", Format = OptionFormat.EqualsSeparated)] - public string? Server { get; set; } - /// /// Colorize output. Choices are: always, never, raw, auto (default "auto") /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPackageGetSchemaOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPackageGetSchemaOptions.Generated.cs index 611b34b8fce..0e3c69a9151 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPackageGetSchemaOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPackageGetSchemaOptions.Generated.cs @@ -34,12 +34,6 @@ public record PulumiPackageGetSchemaOptions( [CliFlag("--help", ShortForm = "-h")] public bool? Help { get; set; } - /// - /// A URL to download the plugin from. When set, the provider argument is used as the plugin name directly and no package resolution is performed. - /// - [CliOption("--server", Format = OptionFormat.EqualsSeparated)] - public string? Server { get; set; } - /// /// Colorize output. Choices are: always, never, raw, auto (default "auto") /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPackageInfoOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPackageInfoOptions.Generated.cs index b591c7f4ba3..7eeb1564a65 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPackageInfoOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPackageInfoOptions.Generated.cs @@ -52,12 +52,6 @@ public record PulumiPackageInfoOptions( [CliOption("--resource", ShortForm = "-r", Format = OptionFormat.EqualsSeparated)] public string? Resource { get; set; } - /// - /// A URL to download the plugin from. When set, the provider argument is used as the plugin name directly and no package resolution is performed. - /// - [CliOption("--server", Format = OptionFormat.EqualsSeparated)] - public string? Server { get; set; } - /// /// Colorize output. Choices are: always, never, raw, auto (default "auto") /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPackagePublishOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPackagePublishOptions.Generated.cs index 9da2c35b9bb..67070d23606 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPackagePublishOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPackagePublishOptions.Generated.cs @@ -46,12 +46,6 @@ public record PulumiPackagePublishOptions( [CliOption("--readme", Format = OptionFormat.EqualsSeparated)] public string? Readme { get; set; } - /// - /// A URL to download the plugin from. When set, the provider argument is used as the plugin name directly and no package resolution is performed. - /// - [CliOption("--server", Format = OptionFormat.EqualsSeparated)] - public string? Server { get; set; } - /// /// Colorize output. Choices are: always, never, raw, auto (default "auto") /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPluginInstallOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPluginInstallOptions.Generated.cs index 937a6f3ac6a..83db231c873 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPluginInstallOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPluginInstallOptions.Generated.cs @@ -135,21 +135,9 @@ public record PulumiPluginInstallOptions : PulumiOptions public int? Verbose { get; set; } /// - /// The kind operand. + /// The kind name [version] operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public string? Kind { get; set; } - - /// - /// The name operand. - /// - [CliArgument(1, Phase = CommandLinePhase.EarlyOperand)] - public string? Name { get; set; } - - /// - /// The version operand. - /// - [CliArgument(2, Phase = CommandLinePhase.EarlyOperand)] - public string? Version { get; set; } + public string? KindNameVersion { get; set; } } diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPreviewOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPreviewOptions.Generated.cs index 80dfe8d0955..7bb3de30b87 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPreviewOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPreviewOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; -using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -25,7 +24,7 @@ public record PulumiPreviewOptions : PulumiOptions /// Enable the ability to attach a debugger to the program and source based plugins being executed. Can limit debug type to 'program', 'plugins', 'plugin:<name>' or 'all'. /// [CliOption("--attach-debugger", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public IEnumerable? AttachDebugger { get; set; } + public IEnumerable? AttachDebugger { get; set; } /// /// Config to use during the preview and save to the stack config file @@ -81,12 +80,6 @@ public record PulumiPreviewOptions : PulumiOptions [CliFlag("--help", ShortForm = "-h")] public bool? Help { get; set; } - /// - /// Ignore the protect resource option for this operation, previewing the deletion or replacement of protected resources instead of failing - /// - [CliFlag("--ignore-protect")] - public bool? IgnoreProtect { get; set; } - /// /// Save any creates seen during the preview into an import file to use with 'pulumi import' /// @@ -139,7 +132,7 @@ public record PulumiPreviewOptions : PulumiOptions /// Refresh the state of the stack's resources before this update /// [CliOption("--refresh", ShortForm = "-r", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? Refresh { get; set; } + public string? Refresh { get; set; } /// /// Specify resources to replace. Multiple resources can be specified using --replace urn1 --replace urn2 @@ -230,7 +223,7 @@ public record PulumiPreviewOptions : PulumiOptions /// Suppress display of the state permalink /// [CliOption("--suppress-permalink", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? SuppressPermalink { get; set; } + public string? SuppressPermalink { get; set; } /// /// Suppress display of periodic progress dots diff --git a/src/ModularPipelines.Pulumi/Options/PulumiProjectNewOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiProjectNewOptions.Generated.cs index d27450855b1..f38511af557 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiProjectNewOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiProjectNewOptions.Generated.cs @@ -20,6 +20,12 @@ namespace ModularPipelines.Pulumi.Options; [CliSubCommand("project", "new")] public record PulumiProjectNewOptions : PulumiOptions { + /// + /// Prompt to use for Pulumi AI + /// + [CliOption("--ai", Format = OptionFormat.EqualsSeparated)] + public string? Ai { get; set; } + /// /// Config to save /// @@ -62,6 +68,12 @@ public record PulumiProjectNewOptions : PulumiOptions [CliFlag("--help", ShortForm = "-h")] public bool? Help { get; set; } + /// + /// Language to use for Pulumi AI (must be one of TypeScript, JavaScript, Python, Go, C#, Java, or YAML) + /// + [CliOption("--language", Format = OptionFormat.EqualsSeparated)] + public string? Language { get; set; } + /// /// List locally installed templates and exit /// @@ -98,6 +110,12 @@ public record PulumiProjectNewOptions : PulumiOptions [CliOption("--stack", ShortForm = "-s", Format = OptionFormat.EqualsSeparated)] public string? Stack { get; set; } + /// + /// Run in template mode, which will skip prompting for AI or Template functionality + /// + [CliFlag("--template-mode", ShortForm = "-t")] + public bool? TemplateMode { get; set; } + /// /// Skip prompts and proceed with default values /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiRefreshOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiRefreshOptions.Generated.cs index b7fbe7784d6..a6342e9aae8 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiRefreshOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiRefreshOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; -using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -181,7 +180,7 @@ public record PulumiRefreshOptions : PulumiOptions /// Suppress display of the state permalink /// [CliOption("--suppress-permalink", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? SuppressPermalink { get; set; } + public string? SuppressPermalink { get; set; } /// /// Suppress display of periodic progress dots diff --git a/src/ModularPipelines.Pulumi/Options/PulumiStackNewOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiStackNewOptions.Generated.cs index da6483c4614..e343bbde426 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiStackNewOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiStackNewOptions.Generated.cs @@ -18,9 +18,7 @@ namespace ModularPipelines.Pulumi.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("stack", "new")] -public record PulumiStackNewOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string OrgNameOrStackName -) : PulumiOptions +public record PulumiStackNewOptions : PulumiOptions { /// /// The name of the stack to copy existing config from @@ -142,4 +140,10 @@ public record PulumiStackNewOptions( [CliOption("--verbose", ShortForm = "-v", Format = OptionFormat.EqualsSeparated)] public int? Verbose { get; set; } + /// + /// The [org-name Or ]<stack-name> operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? OrgNameOrStackName { get; set; } + } diff --git a/src/ModularPipelines.Pulumi/Options/PulumiUpOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiUpOptions.Generated.cs index cf058a50234..15469a750fd 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiUpOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiUpOptions.Generated.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; -using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -25,7 +24,7 @@ public record PulumiUpOptions : PulumiOptions /// Enable the ability to attach a debugger to the program and source based plugins being executed. Can limit debug type to 'program', 'plugins', 'plugin:<name>' or 'all'. /// [CliOption("--attach-debugger", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public IEnumerable? AttachDebugger { get; set; } + public IEnumerable? AttachDebugger { get; set; } /// /// Config to use during the update and save to the stack config file @@ -87,12 +86,6 @@ public record PulumiUpOptions : PulumiOptions [CliFlag("--help", ShortForm = "-h")] public bool? Help { get; set; } - /// - /// Ignore the protect resource option for this operation, allowing protected resources to be deleted or replaced. Use with caution: deleted resources cannot be recovered - /// - [CliFlag("--ignore-protect")] - public bool? IgnoreProtect { get; set; } - /// /// Serialize the update diffs, operations, and overall output as JSON /// @@ -139,7 +132,7 @@ public record PulumiUpOptions : PulumiOptions /// Refresh the state of the stack's resources before this update /// [CliOption("--refresh", ShortForm = "-r", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? Refresh { get; set; } + public string? Refresh { get; set; } /// /// Specify a single resource URN to replace. Multiple resources can be specified using --replace urn1 --replace urn2. Wildcards (*, **) are also supported @@ -241,7 +234,7 @@ public record PulumiUpOptions : PulumiOptions /// Suppress display of the state permalink /// [CliOption("--suppress-permalink", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public CliOptionValue? SuppressPermalink { get; set; } + public string? SuppressPermalink { get; set; } /// /// Suppress display of periodic progress dots diff --git a/src/ModularPipelines.Pulumi/Services/IPulumiStack.Generated.cs b/src/ModularPipelines.Pulumi/Services/IPulumiStack.Generated.cs index bb08e3299a4..6b13a6ef0f3 100644 --- a/src/ModularPipelines.Pulumi/Services/IPulumiStack.Generated.cs +++ b/src/ModularPipelines.Pulumi/Services/IPulumiStack.Generated.cs @@ -125,7 +125,7 @@ public interface IPulumiStack /// The execution configuration options. /// Cancellation token. /// The command result. - Task NewAsync(PulumiStackNewOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default); + Task NewAsync(PulumiStackNewOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default); /// /// Show a stack's output properties. diff --git a/src/ModularPipelines.Pulumi/Services/PulumiStack.Generated.cs b/src/ModularPipelines.Pulumi/Services/PulumiStack.Generated.cs index 1b55fd5dd60..e763c78e873 100644 --- a/src/ModularPipelines.Pulumi/Services/PulumiStack.Generated.cs +++ b/src/ModularPipelines.Pulumi/Services/PulumiStack.Generated.cs @@ -193,11 +193,11 @@ public virtual async Task MigrateAsync( /// Cancellation token. /// The command result. public virtual async Task NewAsync( - PulumiStackNewOptions options, + PulumiStackNewOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options ?? new PulumiStackNewOptions(), executionOptions, cancellationToken); } /// diff --git a/src/ModularPipelines.Rust/Options/CargoAddOptions.Generated.cs b/src/ModularPipelines.Rust/Options/CargoAddOptions.Generated.cs index 1b261faed45..479d78f1579 100644 --- a/src/ModularPipelines.Rust/Options/CargoAddOptions.Generated.cs +++ b/src/ModularPipelines.Rust/Options/CargoAddOptions.Generated.cs @@ -20,7 +20,7 @@ namespace ModularPipelines.Rust.Options; [ExcludeFromCodeCoverage] [CliSubCommand("add")] public record CargoAddOptions( - [property: CliArgument(0, Phase = CommandLinePhase.Passthrough, Required = true)] IEnumerable Dep + [property: CliArgument(0, Phase = CommandLinePhase.Passthrough, Required = true)] IEnumerable DepVersion ) : CargoOptions { } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoCopyOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoCopyOptions.Generated.cs index 80fef9f52cd..3178894522b 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoCopyOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoCopyOptions.Generated.cs @@ -234,6 +234,7 @@ public record SkopeoCopyOptions( /// /// Read a passphrase for signing an image from PATH /// + [SecretValue] [CliOption("--sign-passphrase-file", Format = OptionFormat.EqualsSeparated)] public string? SignPassphraseFile { get; set; } @@ -303,6 +304,6 @@ public record SkopeoCopyOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public IEnumerable? CommandOptions { get; set; } + public string? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoDeleteOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoDeleteOptions.Generated.cs index b66b9e9c0e7..24016a740fc 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoDeleteOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoDeleteOptions.Generated.cs @@ -100,6 +100,6 @@ public record SkopeoDeleteOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public IEnumerable? CommandOptions { get; set; } + public string? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoGenerateSigstoreKeyOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoGenerateSigstoreKeyOptions.Generated.cs index a4fd7ca4405..dbfff0b2e05 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoGenerateSigstoreKeyOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoGenerateSigstoreKeyOptions.Generated.cs @@ -37,6 +37,7 @@ public record SkopeoGenerateSigstoreKeyOptions( /// /// Read a passphrase for the private key from PATH /// + [SecretValue] [CliOption("--passphrase-file", Format = OptionFormat.EqualsSeparated)] public string? PassphraseFile { get; set; } @@ -44,6 +45,6 @@ public record SkopeoGenerateSigstoreKeyOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public IEnumerable? CommandOptions { get; set; } + public string? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoInspectOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoInspectOptions.Generated.cs index e5887e0ce61..9d4cf4c7dcf 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoInspectOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoInspectOptions.Generated.cs @@ -124,6 +124,6 @@ public record SkopeoInspectOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public IEnumerable? CommandOptions { get; set; } + public string? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoListTagsOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoListTagsOptions.Generated.cs index e266ef7a56f..93e322250a8 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoListTagsOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoListTagsOptions.Generated.cs @@ -88,6 +88,6 @@ public record SkopeoListTagsOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public IEnumerable? CommandOptions { get; set; } + public string? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoLoginOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoLoginOptions.Generated.cs index 4c355e92314..00b81777250 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoLoginOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoLoginOptions.Generated.cs @@ -87,6 +87,6 @@ public record SkopeoLoginOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public IEnumerable? CommandOptions { get; set; } + public string? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoLogoutOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoLogoutOptions.Generated.cs index 9d11a6e29d5..dad3ee3deec 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoLogoutOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoLogoutOptions.Generated.cs @@ -56,6 +56,6 @@ public record SkopeoLogoutOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public IEnumerable? CommandOptions { get; set; } + public string? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoStandaloneSignOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoStandaloneSignOptions.Generated.cs index 228be82744a..343afad0123 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoStandaloneSignOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoStandaloneSignOptions.Generated.cs @@ -40,6 +40,7 @@ public record SkopeoStandaloneSignOptions( /// /// file that contains a passphrase for the --sign-by key /// + [SecretValue] [CliOption("--passphrase-file", Format = OptionFormat.EqualsSeparated)] public string? PassphraseFile { get; set; } @@ -47,6 +48,6 @@ public record SkopeoStandaloneSignOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public IEnumerable? CommandOptions { get; set; } + public string? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoSyncOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoSyncOptions.Generated.cs index 49365bd9cc9..9ba28e06cd1 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoSyncOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoSyncOptions.Generated.cs @@ -169,6 +169,7 @@ public record SkopeoSyncOptions( /// /// File that contains a passphrase for the --sign-by key /// + [SecretValue] [CliOption("--sign-passphrase-file", Format = OptionFormat.EqualsSeparated)] public string? SignPassphraseFile { get; set; } @@ -232,6 +233,6 @@ public record SkopeoSyncOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public IEnumerable? CommandOptions { get; set; } + public string? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Trivy/Options/TrivyFilesystemOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyFilesystemOptions.Generated.cs index 1f0afe3ed23..2ad4b612f92 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyFilesystemOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyFilesystemOptions.Generated.cs @@ -510,6 +510,7 @@ public record TrivyFilesystemOptions( /// /// specify a path to config file for secret scanning (default "trivy-secret.yaml") /// + [SecretValue] [CliOption("--secret-config", Format = OptionFormat.EqualsSeparated)] public string? SecretConfig { get; set; } diff --git a/src/ModularPipelines.Trivy/Options/TrivyImageOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyImageOptions.Generated.cs index 125a6cb9bd4..e71a8d65298 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyImageOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyImageOptions.Generated.cs @@ -544,6 +544,7 @@ public record TrivyImageOptions : TrivyOptions /// /// specify a path to config file for secret scanning (default "trivy-secret.yaml") /// + [SecretValue] [CliOption("--secret-config", Format = OptionFormat.EqualsSeparated)] public string? SecretConfig { get; set; } diff --git a/src/ModularPipelines.Trivy/Options/TrivyKubernetesOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyKubernetesOptions.Generated.cs index b6e22a7caee..02f2544ecc9 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyKubernetesOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyKubernetesOptions.Generated.cs @@ -498,6 +498,7 @@ public record TrivyKubernetesOptions : TrivyOptions /// /// specify a path to config file for secret scanning (default "trivy-secret.yaml") /// + [SecretValue] [CliOption("--secret-config", Format = OptionFormat.EqualsSeparated)] public string? SecretConfig { get; set; } diff --git a/src/ModularPipelines.Trivy/Options/TrivyModuleOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyModuleOptions.Generated.cs index f96996dbcbd..9d40aec64e5 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyModuleOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyModuleOptions.Generated.cs @@ -92,4 +92,10 @@ public record TrivyModuleOptions : TrivyOptions [CliFlag("--version", ShortForm = "-v")] public bool? Version { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Trivy/Options/TrivyPluginOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyPluginOptions.Generated.cs index d7f2d35cb5a..eb35167b424 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyPluginOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyPluginOptions.Generated.cs @@ -80,4 +80,10 @@ public record TrivyPluginOptions : TrivyOptions [CliFlag("--version", ShortForm = "-v")] public bool? Version { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Trivy/Options/TrivyRegistryOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyRegistryOptions.Generated.cs index 952f7f1457b..c4a65cadfee 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyRegistryOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyRegistryOptions.Generated.cs @@ -80,4 +80,10 @@ public record TrivyRegistryOptions : TrivyOptions [CliFlag("--version", ShortForm = "-v")] public bool? Version { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Trivy/Options/TrivyRepositoryOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyRepositoryOptions.Generated.cs index 49e13f7c3ed..126b66fe132 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyRepositoryOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyRepositoryOptions.Generated.cs @@ -492,6 +492,7 @@ public record TrivyRepositoryOptions( /// /// specify a path to config file for secret scanning (default "trivy-secret.yaml") /// + [SecretValue] [CliOption("--secret-config", Format = OptionFormat.EqualsSeparated)] public string? SecretConfig { get; set; } diff --git a/src/ModularPipelines.Trivy/Options/TrivyRootfsOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyRootfsOptions.Generated.cs index 66219f15d3d..012faa8834c 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyRootfsOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyRootfsOptions.Generated.cs @@ -498,6 +498,7 @@ public record TrivyRootfsOptions( /// /// specify a path to config file for secret scanning (default "trivy-secret.yaml") /// + [SecretValue] [CliOption("--secret-config", Format = OptionFormat.EqualsSeparated)] public string? SecretConfig { get; set; } diff --git a/src/ModularPipelines.Trivy/Options/TrivyVexOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyVexOptions.Generated.cs index 878e31680e0..2017d492db6 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyVexOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyVexOptions.Generated.cs @@ -80,4 +80,10 @@ public record TrivyVexOptions : TrivyOptions [CliFlag("--version", ShortForm = "-v")] public bool? Version { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Trivy/Options/TrivyVexRepoOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyVexRepoOptions.Generated.cs index ef64c50237f..991cacf23aa 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyVexRepoOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyVexRepoOptions.Generated.cs @@ -80,4 +80,10 @@ public record TrivyVexRepoOptions : TrivyOptions [CliFlag("--version", ShortForm = "-v")] public bool? Version { get; set; } + /// + /// The command operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Command { get; set; } + } diff --git a/src/ModularPipelines.Trivy/Options/TrivyVmOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyVmOptions.Generated.cs index 4b48eb3110e..28986eba79f 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyVmOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyVmOptions.Generated.cs @@ -406,6 +406,7 @@ public record TrivyVmOptions( /// /// specify a path to config file for secret scanning (default "trivy-secret.yaml") /// + [SecretValue] [CliOption("--secret-config", Format = OptionFormat.EqualsSeparated)] public string? SecretConfig { get; set; } From 34708b4504224dadf1ab7215d588f6fa84548f3a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:12:55 +0100 Subject: [PATCH 02/21] fix(generator): preserve generated APIs --- .../CodeGeneratorOrchestratorTests.cs | 52 +++ .../Generators/GeneratorHardeningTests.cs | 234 +++++++++++ .../Generators/CodeGeneratorOrchestrator.cs | 9 +- .../GeneratedApiCompatibilityPreserver.cs | 386 ++++++++++++++++++ 4 files changed, 678 insertions(+), 3 deletions(-) create mode 100644 tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/CodeGeneratorOrchestratorTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/CodeGeneratorOrchestratorTests.cs index 5ffd1ad1a64..937d9b04a4c 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/CodeGeneratorOrchestratorTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/CodeGeneratorOrchestratorTests.cs @@ -145,6 +145,58 @@ public async Task Cli_Metadata_Is_Preserved_For_Generators() } } + [Test] + public async Task Existing_Generated_Options_Are_Used_As_Api_Baseline() + { + var outputRoot = Path.Combine(Path.GetTempPath(), "mp-orchestrator-tests", Guid.NewGuid().ToString("N")); + var scraper = new FakeCliScraper { Commands = [FakeCommand()] }; + await Orchestrator(scraper, new FakeGenerator()).GenerateAsync("fake", outputRoot); + var optionsDirectory = Path.Combine(outputRoot, ToolOutputDirectory, "Options"); + Directory.CreateDirectory(optionsDirectory); + await File.WriteAllTextAsync( + Path.Combine(optionsDirectory, "FakeRunOptions.Generated.cs"), + """ + // + // Generated compatibility baseline. + // + + using ModularPipelines.Attributes; + + public record FakeRunOptions + { + [CliFlag("--removed")] + public bool? Removed { get; set; } + } + """); + CliToolDefinition? generatedTool = null; + var generator = new FakeGenerator + { + OnGenerate = tool => + { + generatedTool = tool; + return []; + }, + }; + + try + { + var result = await Orchestrator(scraper, generator).GenerateAsync("fake", outputRoot); + + await Assert.That(result.HasErrors).IsFalse(); + var compatibilityProperty = generatedTool!.Commands.Single().CompatibilityProperties.Single(); + using (Assert.Multiple()) + { + await Assert.That(compatibilityProperty.PropertyName).IsEqualTo("Removed"); + await Assert.That(compatibilityProperty.CSharpType).IsEqualTo("bool?"); + await Assert.That(compatibilityProperty.ForwardToPropertyName).IsNull(); + } + } + finally + { + Directory.Delete(outputRoot, recursive: true); + } + } + [Test] public async Task Catalog_Metadata_Is_Applied_Before_Every_Generator() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs index 2208e477dbd..f5080a7e649 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs @@ -100,6 +100,22 @@ private static CliToolDefinition Tool(params CliCommandDefinition[] commands) => Commands = commands, }; + private static GeneratedApiProperty BaselineProperty( + string propertyName, + string cSharpType, + string? switchName = null, + int? argumentPosition = null, + bool isRequired = false) => + new( + propertyName, + cSharpType, + switchName, + argumentPosition, + isRequired, + false, + null, + null); + [Test] public async Task Command_Facade_Generation_Can_Be_Disabled_Independently_Of_Options() { @@ -789,6 +805,224 @@ public async Task OptionsClassGenerator_Renames_Global_Compatibility_Targets() } } + [Test] + public async Task ApiCompatibilityPreserver_Retains_Removed_Scraped_Options() + { + var command = Command("ToolBuildOptions", "ToolOptions", ["build"]); + var preserved = GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("RemovedFlag", "bool?", switchName: "--removed-flag")]); + + var generated = (await new OptionsClassGenerator().GenerateAsync(Tool(preserved))).Single().Content; + + using (Assert.Multiple()) + { + await Assert.That(generated).Contains("public bool? RemovedFlag { get; set; }"); + await Assert.That(generated).Contains("RemovedFlag is no longer supported"); + await Assert.That(generated).DoesNotContain("CliFlag(\"--removed-flag\")"); + } + } + + [Test] + public async Task ApiCompatibilityPreserver_Restores_Required_Positional_Names() + { + var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with + { + PositionalArguments = + [ + new CliPositionalArgument + { + PropertyName = "Dep", + CSharpType = "IEnumerable", + IsRequired = true, + PositionIndex = 0, + }, + ], + }; + var preserved = GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty( + "DepVersion", + "IEnumerable", + argumentPosition: 0, + isRequired: true)]); + + var generated = (await new OptionsClassGenerator().GenerateAsync(Tool(preserved))).Single().Content; + + using (Assert.Multiple()) + { + await Assert.That(generated).Contains("IEnumerable DepVersion"); + await Assert.That(generated).Contains("public IEnumerable Dep"); + await Assert.That(generated).Contains("get => DepVersion;"); + } + } + + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Scalar_To_Collection_Changes() + { + var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with + { + Options = + [ + new CliOptionDefinition + { + SwitchName = "--command-options", + PropertyName = "CommandOptions", + CSharpType = "IEnumerable?", + }, + ], + }; + + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("CommandOptions", "string?", switchName: "--command-options")])); + + await Assert.That(exception.Message) + .Contains("ToolCopyOptions.CommandOptions changed type from string? to IEnumerable?"); + } + + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Renamed_Scalar_To_Collection_Changes() + { + var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with + { + Options = + [ + new CliOptionDefinition + { + SwitchName = "--command-options", + PropertyName = "CommandOptionValues", + CSharpType = "IEnumerable?", + }, + ], + }; + + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("CommandOptions", "string?", switchName: "--command-options")])); + + await Assert.That(exception.Message) + .Contains("changed type from string? to IEnumerable? while being renamed"); + } + + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Reassigned_Property_Names() + { + var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with + { + Options = + [ + new CliOptionDefinition + { + SwitchName = "--different", + PropertyName = "CommandOptions", + CSharpType = "string?", + }, + ], + }; + + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("CommandOptions", "string?", switchName: "--command-options")])); + + await Assert.That(exception.Message) + .Contains("ToolCopyOptions.CommandOptions changed CLI switch or argument position"); + } + + [Test] + public async Task ApiCompatibilityPreserver_Uses_The_Emitted_Optional_Value_Type() + { + var command = Command("ToolRunOptions", "ToolOptions", ["run"]) with + { + Options = + [ + new CliOptionDefinition + { + SwitchName = "--progress", + PropertyName = "Progress", + CSharpType = "string?", + ValueArity = CliOptionValueArity.Optional, + }, + ], + }; + + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("Progress", "string?", switchName: "--progress")])); + + await Assert.That(exception.Message) + .Contains("ToolRunOptions.Progress changed type from string? to CliOptionValue?"); + } + + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Optional_To_Required_Changes() + { + var command = Command("ToolNewOptions", "ToolOptions", ["new"]) with + { + PositionalArguments = + [ + new CliPositionalArgument + { + PropertyName = "Name", + CSharpType = "string", + IsRequired = true, + PositionIndex = 0, + }, + ], + }; + + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("Name", "string", argumentPosition: 0)])); + + await Assert.That(exception.Message).Contains("ToolNewOptions.Name changed from optional to required"); + } + + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Removed_Required_Members() + { + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve( + Command("ToolAddOptions", "ToolOptions", ["add"]), + [BaselineProperty( + "Package", + "string", + argumentPosition: 0, + isRequired: true)])); + + await Assert.That(exception.Message) + .Contains("ToolAddOptions.Package was removed from the required constructor"); + } + + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Added_Required_Members() + { + var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with + { + PositionalArguments = + [ + new CliPositionalArgument + { + PropertyName = "Package", + CSharpType = "string", + IsRequired = true, + PositionIndex = 0, + }, + ], + }; + + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve(command, [])); + + await Assert.That(exception.Message) + .Contains("ToolAddOptions.Package was added to the required constructor"); + } + [Test] public async Task OptionsClassGenerator_Marks_Secret_Positional_Arguments() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/CodeGeneratorOrchestrator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/CodeGeneratorOrchestrator.cs index a360b9c56ba..b84390a1968 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/CodeGeneratorOrchestrator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/CodeGeneratorOrchestrator.cs @@ -834,13 +834,16 @@ private async Task GenerateForToolAsync( { var globalOptions = tool.GetGlobalOptions(); var normalizedCommands = GeneratorUtils.NormalizeCommandClassNames(tool.Commands); - var toolDefinition = InheritedPropertyCollisionResolver.Resolve( - ExecutablePrerequisiteCatalog.PrepareForGeneration(tool with + var compatibleTool = GeneratedApiCompatibilityPreserver.Preserve( + tool with { Commands = normalizedCommands, GlobalOptions = globalOptions, SupplementalGlobalOptions = [], - })); + }, + outputDirectory); + var toolDefinition = InheritedPropertyCollisionResolver.Resolve( + ExecutablePrerequisiteCatalog.PrepareForGeneration(compatibleTool)); if (enforceOutputContainment) { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs new file mode 100644 index 00000000000..71d99f2c4b1 --- /dev/null +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -0,0 +1,386 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using ModularPipelines.OptionsGenerator.Models; + +namespace ModularPipelines.OptionsGenerator.Generators; + +internal static class GeneratedApiCompatibilityPreserver +{ + public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDirectory) + { + var optionsDirectory = Path.Combine( + outputDirectory, + tool.OutputDirectory, + "Options"); + if (!Directory.Exists(optionsDirectory)) + { + return tool; + } + + var baseline = ReadBaseline(optionsDirectory); + return tool with + { + Commands = tool.Commands + .Select(command => baseline.TryGetValue(command.ClassName, out var properties) + ? Preserve(command, properties) + : command) + .ToArray(), + }; + } + + internal static CliCommandDefinition Preserve( + CliCommandDefinition command, + IReadOnlyList baselineProperties) + { + var compatibilityProperties = command.CompatibilityProperties.ToList(); + var positionalArguments = command.PositionalArguments.ToArray(); + var options = command.Options.ToArray(); + + RestoreRequiredMemberNames( + baselineProperties, + positionalArguments, + options, + compatibilityProperties); + + var currentProperties = GetCurrentProperties(positionalArguments, options); + var violations = new List(); + foreach (var baseline in baselineProperties) + { + if (baseline.IsCompatibility) + { + AddCompatibilityProperty( + compatibilityProperties, + new CliCompatibilityProperty + { + PropertyName = baseline.PropertyName, + CSharpType = baseline.CSharpType, + ForwardToPropertyName = baseline.ForwardToPropertyName, + ObsoleteMessage = baseline.ObsoleteMessage + ?? $"{baseline.PropertyName} is retained for compatibility.", + }); + continue; + } + + var sameName = currentProperties.FirstOrDefault(property => + property.PropertyName.Equals(baseline.PropertyName, StringComparison.Ordinal)); + if (sameName is not null) + { + if (!sameName.CSharpType.Equals(baseline.CSharpType, StringComparison.Ordinal)) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} changed type from " + + $"{baseline.CSharpType} to {sameName.CSharpType}"); + } + else if (!baseline.IsRequired && sameName.IsRequired) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} changed from optional to required"); + } + else if (!HasSameCliIdentity(sameName, baseline)) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} changed CLI switch or argument position"); + } + + continue; + } + + var sameCliMember = currentProperties.FirstOrDefault(property => + HasSameCliIdentity(property, baseline)); + if (sameCliMember is not null + && !sameCliMember.CSharpType.Equals(baseline.CSharpType, StringComparison.Ordinal)) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} changed type from " + + $"{baseline.CSharpType} to {sameCliMember.CSharpType} " + + $"while being renamed to {sameCliMember.PropertyName}"); + continue; + } + + var replacement = sameCliMember; + if (baseline.IsRequired && replacement is null) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} was removed from the required constructor"); + continue; + } + + AddCompatibilityProperty( + compatibilityProperties, + new CliCompatibilityProperty + { + PropertyName = baseline.PropertyName, + CSharpType = baseline.CSharpType, + ForwardToPropertyName = replacement?.PropertyName, + ObsoleteMessage = replacement is null + ? $"{baseline.PropertyName} is no longer supported by the installed CLI and has no effect." + : $"Use {replacement.PropertyName} instead.", + }); + } + + foreach (var current in currentProperties.Where(static property => property.IsRequired)) + { + if (baselineProperties.Any(baseline => + !baseline.IsCompatibility + && baseline.PropertyName.Equals(current.PropertyName, StringComparison.Ordinal))) + { + continue; + } + + violations.Add( + $"{command.ClassName}.{current.PropertyName} was added to the required constructor"); + } + + if (violations.Count > 0) + { + throw new InvalidOperationException( + $"Generated API compatibility validation failed for {command.FullCommand}:" + + Environment.NewLine + + string.Join(Environment.NewLine, violations.Select(violation => $"- {violation}"))); + } + + return command with + { + Options = options, + PositionalArguments = positionalArguments, + CompatibilityProperties = compatibilityProperties, + }; + } + + private static void RestoreRequiredMemberNames( + IReadOnlyList baselineProperties, + CliPositionalArgument[] positionalArguments, + CliOptionDefinition[] options, + ICollection compatibilityProperties) + { + foreach (var baseline in baselineProperties.Where(property => + property.IsRequired && !property.IsCompatibility)) + { + var positionalIndex = Array.FindIndex(positionalArguments, current => + current.IsRequired + && HasSameCliIdentity(ToGeneratedProperty(current), baseline) + && ToGeneratedProperty(current).CSharpType.Equals( + baseline.CSharpType, + StringComparison.Ordinal)); + if (positionalIndex >= 0 + && !positionalArguments[positionalIndex].PropertyName.Equals( + baseline.PropertyName, + StringComparison.Ordinal)) + { + var currentName = positionalArguments[positionalIndex].PropertyName; + positionalArguments[positionalIndex] = positionalArguments[positionalIndex] with + { + PropertyName = baseline.PropertyName, + }; + AddRenamedCurrentProperty( + compatibilityProperties, + currentName, + baseline.CSharpType, + baseline.PropertyName); + continue; + } + + var optionIndex = Array.FindIndex(options, current => + current.IsRequired + && HasSameCliIdentity(ToGeneratedProperty(current), baseline) + && ToGeneratedProperty(current).CSharpType.Equals( + baseline.CSharpType, + StringComparison.Ordinal)); + if (optionIndex < 0 + || options[optionIndex].PropertyName.Equals(baseline.PropertyName, StringComparison.Ordinal)) + { + continue; + } + + var optionName = options[optionIndex].PropertyName; + options[optionIndex] = options[optionIndex] with { PropertyName = baseline.PropertyName }; + AddRenamedCurrentProperty( + compatibilityProperties, + optionName, + baseline.CSharpType, + baseline.PropertyName); + } + } + + private static void AddRenamedCurrentProperty( + ICollection compatibilityProperties, + string propertyName, + string cSharpType, + string forwardToPropertyName) => + AddCompatibilityProperty( + compatibilityProperties, + new CliCompatibilityProperty + { + PropertyName = propertyName, + CSharpType = cSharpType, + ForwardToPropertyName = forwardToPropertyName, + ObsoleteMessage = $"Use {forwardToPropertyName} instead.", + }); + + private static void AddCompatibilityProperty( + ICollection compatibilityProperties, + CliCompatibilityProperty property) + { + if (compatibilityProperties.Any(existing => + existing.PropertyName.Equals(property.PropertyName, StringComparison.Ordinal))) + { + return; + } + + compatibilityProperties.Add(property); + } + + private static GeneratedApiProperty[] GetCurrentProperties( + IEnumerable positionalArguments, + IEnumerable options) => + positionalArguments.Select(ToGeneratedProperty) + .Concat(options.Select(ToGeneratedProperty)) + .ToArray(); + + private static GeneratedApiProperty ToGeneratedProperty(CliPositionalArgument argument) => + new( + argument.PropertyName, + argument.IsRequired ? argument.CSharpType.TrimEnd('?') : argument.CSharpType, + null, + argument.PositionIndex, + argument.IsRequired, + false, + null, + null); + + private static GeneratedApiProperty ToGeneratedProperty(CliOptionDefinition option) => + new( + option.PropertyName, + option.IsRequired ? option.PropertyType.TrimEnd('?') : option.PropertyType, + option.SwitchName, + null, + option.IsRequired, + false, + null, + null); + + private static bool HasSameCliIdentity( + GeneratedApiProperty left, + GeneratedApiProperty right) + { + if (left.ArgumentPosition is not null || right.ArgumentPosition is not null) + { + return left.ArgumentPosition == right.ArgumentPosition; + } + + if (left.SwitchName is not null || right.SwitchName is not null) + { + return left.SwitchName?.Equals(right.SwitchName, StringComparison.Ordinal) == true; + } + + return true; + } + + private static Dictionary> ReadBaseline( + string optionsDirectory) + { + var baseline = new Dictionary>(StringComparer.Ordinal); + foreach (var path in Directory.EnumerateFiles( + optionsDirectory, + "*.Generated.cs", + SearchOption.TopDirectoryOnly)) + { + var root = CSharpSyntaxTree.ParseText(File.ReadAllText(path)).GetRoot(); + foreach (var declaration in root.DescendantNodes().OfType()) + { + baseline[declaration.Identifier.ValueText] = ReadProperties(declaration); + } + } + + return baseline; + } + + private static List ReadProperties( + RecordDeclarationSyntax declaration) + { + var properties = new List(); + if (declaration.ParameterList is not null) + { + properties.AddRange(declaration.ParameterList.Parameters.Select(parameter => + ReadProperty( + parameter.Identifier.ValueText, + parameter.Type?.ToString() ?? string.Empty, + parameter.AttributeLists, + isRequired: true, + accessorList: null))); + } + + properties.AddRange(declaration.Members + .OfType() + .Where(property => property.Modifiers.Any(SyntaxKind.PublicKeyword)) + .Select(property => ReadProperty( + property.Identifier.ValueText, + property.Type.ToString(), + property.AttributeLists, + isRequired: false, + property.AccessorList))); + return properties; + } + + private static GeneratedApiProperty ReadProperty( + string propertyName, + string cSharpType, + SyntaxList attributeLists, + bool isRequired, + AccessorListSyntax? accessorList) + { + var attributes = attributeLists.SelectMany(list => list.Attributes).ToArray(); + var cliArgument = FindAttribute(attributes, "CliArgument"); + var cliOption = FindAttribute(attributes, "CliOption") + ?? FindAttribute(attributes, "CliFlag"); + var obsolete = FindAttribute(attributes, "Obsolete"); + + return new GeneratedApiProperty( + propertyName, + cSharpType, + GetStringArgument(cliOption), + GetIntegerArgument(cliArgument), + isRequired, + obsolete is not null, + GetForwardTarget(accessorList), + GetStringArgument(obsolete)); + } + + private static AttributeSyntax? FindAttribute( + IEnumerable attributes, + string name) => + attributes.FirstOrDefault(attribute => + attribute.Name.ToString().Equals(name, StringComparison.Ordinal) + || attribute.Name.ToString().Equals($"{name}Attribute", StringComparison.Ordinal)); + + private static string? GetStringArgument(AttributeSyntax? attribute) => + attribute?.ArgumentList?.Arguments.FirstOrDefault()?.Expression is LiteralExpressionSyntax literal + && literal.IsKind(SyntaxKind.StringLiteralExpression) + ? literal.Token.ValueText + : null; + + private static int? GetIntegerArgument(AttributeSyntax? attribute) => + attribute?.ArgumentList?.Arguments.FirstOrDefault()?.Expression is LiteralExpressionSyntax literal + && literal.IsKind(SyntaxKind.NumericLiteralExpression) + && literal.Token.Value is int value + ? value + : null; + + private static string? GetForwardTarget(AccessorListSyntax? accessorList) => + accessorList?.Accessors + .FirstOrDefault(accessor => accessor.IsKind(SyntaxKind.GetAccessorDeclaration))? + .ExpressionBody?.Expression is IdentifierNameSyntax identifier + ? identifier.Identifier.ValueText + : null; +} + +internal sealed record GeneratedApiProperty( + string PropertyName, + string CSharpType, + string? SwitchName, + int? ArgumentPosition, + bool IsRequired, + bool IsCompatibility, + string? ForwardToPropertyName, + string? ObsoleteMessage); From c8210453bca8c14808da6abc47475eb73c0787ac Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:33:48 +0100 Subject: [PATCH 03/21] fix(docker): preserve no-TTY switch casing --- .../DockerComposeExecOptions.Generated.cs | 2 +- .../Scrapers/DockerCliCompatibilityTests.cs | 40 +++++++++++++++++++ .../Scrapers/Cli/CobraCliScraper.cs | 9 +++++ .../Scrapers/Cli/DockerCliScraper.cs | 9 +++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/ModularPipelines.Docker/Options/DockerComposeExecOptions.Generated.cs b/src/ModularPipelines.Docker/Options/DockerComposeExecOptions.Generated.cs index d4d7342e709..3a3c07721bd 100644 --- a/src/ModularPipelines.Docker/Options/DockerComposeExecOptions.Generated.cs +++ b/src/ModularPipelines.Docker/Options/DockerComposeExecOptions.Generated.cs @@ -50,7 +50,7 @@ public record DockerComposeExecOptions( /// /// Disable pseudo-TTY allocation. By default 'docker compose exec' allocates a TTY. (default true) /// - [CliOption("--no-tty", ShortForm = "-T", Format = OptionFormat.EqualsSeparated)] + [CliOption("--no-TTY", ShortForm = "-T", Format = OptionFormat.EqualsSeparated)] public bool? NoTty { get; set; } /// diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs index 35882aaa027..c22ead1ce61 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs @@ -1,5 +1,8 @@ +using Microsoft.Extensions.Logging.Abstractions; using ModularPipelines.OptionsGenerator.Models; using ModularPipelines.OptionsGenerator.Scrapers; +using ModularPipelines.OptionsGenerator.Scrapers.Cli; +using ModularPipelines.OptionsGenerator.TypeDetection; namespace ModularPipelines.OptionsGenerator.Tests.Scrapers; @@ -25,6 +28,26 @@ public async Task SupportedAliases_ExcludeAlias_WhenCanonicalCommandWasNotScrape await Assert.That(aliases).IsEmpty(); } + [Test] + public async Task ComposeExec_Preserves_Canonical_NoTty_Switch_Casing() + { + const string helpText = """ + Execute a command in a running container + + Usage: docker compose exec [OPTIONS] SERVICE COMMAND [ARGS...] + + Options: + -T, --no-tty Disable pseudo-TTY allocation + """; + var command = await new TestDockerCliScraper().Parse( + ["docker", "compose", "exec"], + helpText); + + var option = command!.Options.Single(); + await Assert.That(option.SwitchName).IsEqualTo("--no-TTY"); + await Assert.That(option.ShortForm).IsEqualTo("-T"); + } + private static CliCommandDefinition CreateCommand(string commandGroup) { return new CliCommandDefinition @@ -37,4 +60,21 @@ private static CliCommandDefinition CreateCommand(string commandGroup) Options = [], }; } + + private sealed class TestDockerCliScraper : DockerCliScraper + { + public TestDockerCliScraper() + : base( + new ProcessCliCommandExecutor(NullLogger.Instance), + new HelpTextCache(NullLogger.Instance), + NullLogger.Instance) + { + } + + public Task Parse(string[] commandPath, string helpText) + { + var usage = ParseUsageSynopsis(commandPath, helpText); + return ParseCommandAsync(commandPath, helpText, usage, CancellationToken.None); + } + } } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs index 2c7e915db75..f8996770e23 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs @@ -312,6 +312,8 @@ private List ParseOptions(string helpText, string[] command continue; } + longForm = NormalizeOptionSwitchName(commandParts, longForm); + // Skip duplicates if (seenOptions.Contains(longForm)) { @@ -436,6 +438,13 @@ protected virtual bool IsSecretOption(string propertyName, bool isFlag) => /// protected virtual bool ShouldGenerateEnum(string[] commandParts, string switchName) => true; + /// + /// Applies tool-specific corrections to an option's canonical switch spelling. + /// + protected virtual string NormalizeOptionSwitchName( + string[] commandParts, + string switchName) => switchName; + /// /// Applies tool-specific corrections when CLI help omits or misreports an option type. /// diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/DockerCliScraper.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/DockerCliScraper.cs index c3a74703338..d1eeb4284e1 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/DockerCliScraper.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/DockerCliScraper.cs @@ -31,4 +31,13 @@ public DockerCliScraper(ICliCommandExecutor executor, IHelpTextCache helpCache, string[] commandPath, string helpText) => DockerCliCompatibility.DetectCommandGroupAlias(commandPath, helpText); + + /// + protected override string NormalizeOptionSwitchName( + string[] commandParts, + string switchName) => + commandParts is ["compose", "exec"] + && switchName.Equals("--no-tty", StringComparison.OrdinalIgnoreCase) + ? "--no-TTY" + : switchName; } From 15cda8e77cd1064a2fffc97d49230cb9d8d5f5fe Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:44:47 +0100 Subject: [PATCH 04/21] refactor(generator): split compatibility checks --- .../GeneratedApiCompatibilityPreserver.cs | 197 +++++++++++------- 1 file changed, 116 insertions(+), 81 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index 71d99f2c4b1..91282e909f6 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -47,90 +47,15 @@ internal static CliCommandDefinition Preserve( var violations = new List(); foreach (var baseline in baselineProperties) { - if (baseline.IsCompatibility) - { - AddCompatibilityProperty( - compatibilityProperties, - new CliCompatibilityProperty - { - PropertyName = baseline.PropertyName, - CSharpType = baseline.CSharpType, - ForwardToPropertyName = baseline.ForwardToPropertyName, - ObsoleteMessage = baseline.ObsoleteMessage - ?? $"{baseline.PropertyName} is retained for compatibility.", - }); - continue; - } - - var sameName = currentProperties.FirstOrDefault(property => - property.PropertyName.Equals(baseline.PropertyName, StringComparison.Ordinal)); - if (sameName is not null) - { - if (!sameName.CSharpType.Equals(baseline.CSharpType, StringComparison.Ordinal)) - { - violations.Add( - $"{command.ClassName}.{baseline.PropertyName} changed type from " - + $"{baseline.CSharpType} to {sameName.CSharpType}"); - } - else if (!baseline.IsRequired && sameName.IsRequired) - { - violations.Add( - $"{command.ClassName}.{baseline.PropertyName} changed from optional to required"); - } - else if (!HasSameCliIdentity(sameName, baseline)) - { - violations.Add( - $"{command.ClassName}.{baseline.PropertyName} changed CLI switch or argument position"); - } - - continue; - } - - var sameCliMember = currentProperties.FirstOrDefault(property => - HasSameCliIdentity(property, baseline)); - if (sameCliMember is not null - && !sameCliMember.CSharpType.Equals(baseline.CSharpType, StringComparison.Ordinal)) - { - violations.Add( - $"{command.ClassName}.{baseline.PropertyName} changed type from " - + $"{baseline.CSharpType} to {sameCliMember.CSharpType} " - + $"while being renamed to {sameCliMember.PropertyName}"); - continue; - } - - var replacement = sameCliMember; - if (baseline.IsRequired && replacement is null) - { - violations.Add( - $"{command.ClassName}.{baseline.PropertyName} was removed from the required constructor"); - continue; - } - - AddCompatibilityProperty( + PreserveBaselineProperty( + command, + baseline, + currentProperties, compatibilityProperties, - new CliCompatibilityProperty - { - PropertyName = baseline.PropertyName, - CSharpType = baseline.CSharpType, - ForwardToPropertyName = replacement?.PropertyName, - ObsoleteMessage = replacement is null - ? $"{baseline.PropertyName} is no longer supported by the installed CLI and has no effect." - : $"Use {replacement.PropertyName} instead.", - }); + violations); } - foreach (var current in currentProperties.Where(static property => property.IsRequired)) - { - if (baselineProperties.Any(baseline => - !baseline.IsCompatibility - && baseline.PropertyName.Equals(current.PropertyName, StringComparison.Ordinal))) - { - continue; - } - - violations.Add( - $"{command.ClassName}.{current.PropertyName} was added to the required constructor"); - } + AddNewRequiredMemberViolations(command, baselineProperties, currentProperties, violations); if (violations.Count > 0) { @@ -148,6 +73,116 @@ internal static CliCommandDefinition Preserve( }; } + private static void PreserveBaselineProperty( + CliCommandDefinition command, + GeneratedApiProperty baseline, + IReadOnlyList currentProperties, + ICollection compatibilityProperties, + List violations) + { + if (baseline.IsCompatibility) + { + PreserveCompatibilityProperty(baseline, compatibilityProperties); + return; + } + + var sameName = currentProperties.FirstOrDefault(property => + property.PropertyName.Equals(baseline.PropertyName, StringComparison.Ordinal)); + if (sameName is not null) + { + ValidateMatchingProperty(command, baseline, sameName, violations); + return; + } + + var replacement = currentProperties.FirstOrDefault(property => + HasSameCliIdentity(property, baseline)); + if (replacement is not null + && !replacement.CSharpType.Equals(baseline.CSharpType, StringComparison.Ordinal)) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} changed type from " + + $"{baseline.CSharpType} to {replacement.CSharpType} " + + $"while being renamed to {replacement.PropertyName}"); + return; + } + + if (baseline.IsRequired && replacement is null) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} was removed from the required constructor"); + return; + } + + AddCompatibilityProperty( + compatibilityProperties, + new CliCompatibilityProperty + { + PropertyName = baseline.PropertyName, + CSharpType = baseline.CSharpType, + ForwardToPropertyName = replacement?.PropertyName, + ObsoleteMessage = replacement is null + ? $"{baseline.PropertyName} is no longer supported by the installed CLI and has no effect." + : $"Use {replacement.PropertyName} instead.", + }); + } + + private static void PreserveCompatibilityProperty( + GeneratedApiProperty baseline, + ICollection compatibilityProperties) => + AddCompatibilityProperty( + compatibilityProperties, + new CliCompatibilityProperty + { + PropertyName = baseline.PropertyName, + CSharpType = baseline.CSharpType, + ForwardToPropertyName = baseline.ForwardToPropertyName, + ObsoleteMessage = baseline.ObsoleteMessage + ?? $"{baseline.PropertyName} is retained for compatibility.", + }); + + private static void ValidateMatchingProperty( + CliCommandDefinition command, + GeneratedApiProperty baseline, + GeneratedApiProperty current, + List violations) + { + if (!current.CSharpType.Equals(baseline.CSharpType, StringComparison.Ordinal)) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} changed type from " + + $"{baseline.CSharpType} to {current.CSharpType}"); + } + else if (!baseline.IsRequired && current.IsRequired) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} changed from optional to required"); + } + else if (!HasSameCliIdentity(current, baseline)) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} changed CLI switch or argument position"); + } + } + + private static void AddNewRequiredMemberViolations( + CliCommandDefinition command, + IReadOnlyList baselineProperties, + IEnumerable currentProperties, + List violations) + { + foreach (var current in currentProperties.Where(static property => property.IsRequired)) + { + var existedInBaseline = baselineProperties.Any(baseline => + !baseline.IsCompatibility + && baseline.PropertyName.Equals(current.PropertyName, StringComparison.Ordinal)); + if (!existedInBaseline) + { + violations.Add( + $"{command.ClassName}.{current.PropertyName} was added to the required constructor"); + } + } + } + private static void RestoreRequiredMemberNames( IReadOnlyList baselineProperties, CliPositionalArgument[] positionalArguments, From e3c81465fb5b381a673e5d535bf15731d233efe8 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:58:32 +0100 Subject: [PATCH 05/21] fix(generator): preserve all option APIs Keep current constructor and optional-value APIs while extending baseline preservation to global and supplemental options. --- .../Options/BuildahBuildOptions.Generated.cs | 5 +- .../KustomizeCfgCatOptions.Generated.cs | 11 ++- .../Generators/GeneratorHardeningTests.cs | 79 +++++++++++++++++++ .../GeneratedApiCompatibilityPreserver.cs | 33 +++++++- .../Generators/GeneratorUtils.cs | 20 +++++ .../Generators/GlobalOptionsBaseGenerator.cs | 13 +++ .../InheritedPropertyCollisionResolver.cs | 8 ++ .../Generators/OptionsClassGenerator.cs | 22 +----- .../Models/CliToolDefinition.cs | 5 ++ 9 files changed, 173 insertions(+), 23 deletions(-) diff --git a/src/ModularPipelines.Buildah/Options/BuildahBuildOptions.Generated.cs b/src/ModularPipelines.Buildah/Options/BuildahBuildOptions.Generated.cs index 69ef63b69b4..85383314139 100644 --- a/src/ModularPipelines.Buildah/Options/BuildahBuildOptions.Generated.cs +++ b/src/ModularPipelines.Buildah/Options/BuildahBuildOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Buildah.Options; +using ModularPipelines.Models; namespace ModularPipelines.Buildah.Options; @@ -413,8 +414,8 @@ public record BuildahBuildOptions : BuildahOptions /// /// pull the image from the registry if newer or not present in store, if false, only pull the image if not present, if always, pull the image even if the named image is present in store, if never, only use the image present in store if available (default "true") /// - [CliOption("--pull", Format = OptionFormat.EqualsSeparated)] - public string? Pull { get; set; } + [CliOption("--pull", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] + public CliOptionValue? Pull { get; set; } /// /// refrain from announcing build instructions and image read/write progress diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCatOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCatOptions.Generated.cs index 961330a969a..41d90800cf0 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCatOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgCatOptions.Generated.cs @@ -18,8 +18,17 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("cfg", "cat")] -public record KustomizeCfgCatOptions : KustomizeOptions +public record KustomizeCfgCatOptions( + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Dir +) : KustomizeOptions { + /// + /// Creates compatibility options without the newly required directory operand. + /// + public KustomizeCfgCatOptions() : this(string.Empty) + { + } + /// /// annotate resources with their file origins. /// diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs index f5080a7e649..ebbdc4201bd 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs @@ -1023,6 +1023,85 @@ await Assert.That(exception.Message) .Contains("ToolAddOptions.Package was added to the required constructor"); } + [Test] + public async Task ApiCompatibilityPreserver_Retains_Removed_Global_Options() + { + var preserved = GeneratedApiCompatibilityPreserver.PreserveGlobalOptions( + Tool(Command("ToolRunOptions", "ToolOptions", ["run"])), + [BaselineProperty("LegacyFlag", "bool?", switchName: "--legacy-flag")]); + + var generated = (await new GlobalOptionsBaseGenerator().GenerateAsync(preserved)).Single().Content; + + using (Assert.Multiple()) + { + await Assert.That(generated).Contains("public virtual bool? LegacyFlag { get; set; }"); + await Assert.That(generated).Contains("LegacyFlag is no longer supported"); + await Assert.That(generated).DoesNotContain("CliFlag(\"--legacy-flag\")"); + } + } + + [Test] + public async Task ApiCompatibilityPreserver_Validates_Supplemental_Global_Options() + { + var tool = Tool(Command("ToolRunOptions", "ToolOptions", ["run"])) with + { + SupplementalGlobalOptions = + [ + new CliOptionDefinition + { + SwitchName = "--progress", + PropertyName = "Progress", + CSharpType = "string?", + }, + ], + }; + + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.PreserveGlobalOptions( + tool, + [BaselineProperty("Progress", "CliOptionValue?", switchName: "--progress")])); + + await Assert.That(exception.Message) + .Contains("ToolOptions.Progress changed type from CliOptionValue? to string?"); + } + + [Test] + public async Task Global_Compatibility_Targets_Follow_Inherited_Property_Renames() + { + var tool = Tool(Command("ToolRunOptions", "ToolOptions", ["run"])) with + { + GlobalOptions = + [ + new CliOptionDefinition + { + SwitchName = "--arguments", + PropertyName = "Arguments", + CSharpType = "IEnumerable?", + }, + ], + GlobalCompatibilityProperties = + [ + new CliCompatibilityProperty + { + PropertyName = "LegacyArguments", + CSharpType = "IEnumerable?", + ForwardToPropertyName = "Arguments", + ObsoleteMessage = "Use Arguments instead.", + }, + ], + }; + + var resolved = InheritedPropertyCollisionResolver.Resolve(tool); + var generated = (await new GlobalOptionsBaseGenerator().GenerateAsync(resolved)).Single().Content; + + using (Assert.Multiple()) + { + await Assert.That(generated).Contains("public virtual IEnumerable? CliArguments"); + await Assert.That(generated).Contains("get => CliArguments;"); + await Assert.That(generated).Contains("set => CliArguments = value;"); + } + } + [Test] public async Task OptionsClassGenerator_Marks_Secret_Positional_Arguments() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index 91282e909f6..0302f57e908 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -19,9 +19,12 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi } var baseline = ReadBaseline(optionsDirectory); - return tool with + var compatibleTool = baseline.TryGetValue($"{tool.NamespacePrefix}Options", out var globalProperties) + ? PreserveGlobalOptions(tool, globalProperties) + : tool; + return compatibleTool with { - Commands = tool.Commands + Commands = compatibleTool.Commands .Select(command => baseline.TryGetValue(command.ClassName, out var properties) ? Preserve(command, properties) : command) @@ -29,6 +32,32 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi }; } + internal static CliToolDefinition PreserveGlobalOptions( + CliToolDefinition tool, + IReadOnlyList baselineProperties) + { + var globalClassName = $"{tool.NamespacePrefix}Options"; + var preserved = Preserve( + new CliCommandDefinition + { + FullCommand = tool.ToolName, + CommandParts = [], + ClassName = globalClassName, + ParentClassName = "CommandLineToolOptions", + ToolNamespacePrefix = tool.NamespacePrefix, + Options = tool.GetGlobalOptions(), + CompatibilityProperties = tool.GlobalCompatibilityProperties, + }, + baselineProperties); + + return tool with + { + GlobalOptions = preserved.Options, + SupplementalGlobalOptions = [], + GlobalCompatibilityProperties = preserved.CompatibilityProperties, + }; + } + internal static CliCommandDefinition Preserve( CliCommandDefinition command, IReadOnlyList baselineProperties) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs index 4dc47a4c78c..210e1dd9c52 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs @@ -21,6 +21,26 @@ public static partial class GeneratorUtils @"C:\Users\runneradmin", ]; + internal static void GenerateCompatibilityProperty( + StringBuilder sb, + CliCompatibilityProperty property, + string modifiers = "") + { + sb.AppendLine($" [Obsolete({FormatStringLiteral(property.ObsoleteMessage)})]"); + + if (property.ForwardToPropertyName is null) + { + sb.AppendLine($" public {modifiers}{property.CSharpType} {property.PropertyName} {{ get; set; }}"); + return; + } + + sb.AppendLine($" public {modifiers}{property.CSharpType} {property.PropertyName}"); + sb.AppendLine(" {"); + sb.AppendLine($" get => {property.ForwardToPropertyName};"); + sb.AppendLine($" set => {property.ForwardToPropertyName} = value;"); + sb.AppendLine(" }"); + } + internal readonly record struct RequiredConstructorParameter( string PropertyName, string CSharpType, diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs index 06c035e9c8f..cf56715c477 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs @@ -86,6 +86,19 @@ private static string GenerateBaseOptionsClass(CliToolDefinition tool) sb.AppendLine(); } + foreach (var compatibilityProperty in tool.GlobalCompatibilityProperties) + { + var newModifier = InheritedPropertyCollisionResolver.IsInheritedPropertyName( + compatibilityProperty.PropertyName) + ? "new " + : string.Empty; + GeneratorUtils.GenerateCompatibilityProperty( + sb, + compatibilityProperty, + $"{newModifier}virtual "); + sb.AppendLine(); + } + sb.AppendLine("}"); return sb.ToString(); diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/InheritedPropertyCollisionResolver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/InheritedPropertyCollisionResolver.cs index c9593ce222a..7e9e91de68f 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/InheritedPropertyCollisionResolver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/InheritedPropertyCollisionResolver.cs @@ -21,6 +21,7 @@ public static CliToolDefinition Resolve(CliToolDefinition tool) var globalNames = tool.GlobalOptions .Concat(tool.SupplementalGlobalOptions) .Select(option => option.PropertyName) + .Concat(tool.GlobalCompatibilityProperties.Select(property => property.PropertyName)) .ToHashSet(StringComparer.Ordinal); var globalRenamedProperties = new Dictionary(StringComparer.Ordinal); var globalOptions = ResolveOptions( @@ -36,6 +37,7 @@ public static CliToolDefinition Resolve(CliToolDefinition tool) var resolvedGlobalNames = globalOptions .Concat(supplementalGlobalOptions) .Select(option => option.PropertyName) + .Concat(tool.GlobalCompatibilityProperties.Select(property => property.PropertyName)) .ToHashSet(StringComparer.Ordinal); return tool with @@ -48,6 +50,12 @@ public static CliToolDefinition Resolve(CliToolDefinition tool) .ToArray(), GlobalOptions = globalOptions, SupplementalGlobalOptions = supplementalGlobalOptions, + GlobalCompatibilityProperties = tool.GlobalCompatibilityProperties + .Select(property => property.ForwardToPropertyName is { } target + && globalRenamedProperties.TryGetValue(target, out var renamedTarget) + ? property with { ForwardToPropertyName = renamedTarget } + : property) + .ToArray(), }; } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs index 9a7891e54bf..6c503a856bb 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs @@ -350,7 +350,10 @@ private static void GenerateProperties( continue; } - GenerateCompatibilityProperty(sb, compatibilityProperty); + GeneratorUtils.GenerateCompatibilityProperty( + sb, + compatibilityProperty, + GetNewModifier(compatibilityProperty.PropertyName)); sb.AppendLine(); } } @@ -445,23 +448,6 @@ private static void GeneratePositionalArgument(StringBuilder sb, CliPositionalAr sb.AppendLine($" public {positional.CSharpType} {positional.PropertyName} {{ get; set; }}"); } - private static void GenerateCompatibilityProperty(StringBuilder sb, CliCompatibilityProperty property) - { - sb.AppendLine($" [Obsolete({GeneratorUtils.FormatStringLiteral(property.ObsoleteMessage)})]"); - - if (property.ForwardToPropertyName is null) - { - sb.AppendLine($" public {GetNewModifier(property.PropertyName)}{property.CSharpType} {property.PropertyName} {{ get; set; }}"); - return; - } - - sb.AppendLine($" public {GetNewModifier(property.PropertyName)}{property.CSharpType} {property.PropertyName}"); - sb.AppendLine(" {"); - sb.AppendLine($" get => {property.ForwardToPropertyName};"); - sb.AppendLine($" set => {property.ForwardToPropertyName} = value;"); - sb.AppendLine(" }"); - } - private static string GetNewModifier(string propertyName) => InheritedPropertyCollisionResolver.IsInheritedPropertyName(propertyName) ? "new " : ""; diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliToolDefinition.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliToolDefinition.cs index e8a8b99d5d7..19e84080693 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliToolDefinition.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliToolDefinition.cs @@ -75,6 +75,11 @@ public record CliToolDefinition /// public IReadOnlyList SupplementalGlobalOptions { get; init; } = []; + /// + /// Public global properties retained for source and binary compatibility but excluded from CLI rendering. + /// + public IReadOnlyList GlobalCompatibilityProperties { get; init; } = []; + /// /// Returns the validated, deterministic union of scraped and supplemental global options. /// From 9f12fbba2bf8384ddd57c9fe896c8b188d56ab9d Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:36:06 +0100 Subject: [PATCH 06/21] fix(generator): preserve generated API contracts --- .../Options/BuildahFromOptions.Generated.cs | 5 +- .../FluxPluginInstallOptions.Generated.cs | 2 +- .../KustomizeCfgGrepOptions.Generated.cs | 9 +- .../KustomizeCreateOptions.Generated.cs | 4 +- ...zeEditAddBuildmetadataOptions.Generated.cs | 4 +- ...tomizeEditAddConfigmapOptions.Generated.cs | 6 +- ...KustomizeEditAddSecretOptions.Generated.cs | 6 +- ...ditRemoveBuildmetadataOptions.Generated.cs | 4 +- ...izeEditRemoveConfigmapOptions.Generated.cs | 4 +- ...tomizeEditRemoveSecretOptions.Generated.cs | 4 +- ...zeEditSetBuildmetadataOptions.Generated.cs | 4 +- ...tomizeEditSetConfigmapOptions.Generated.cs | 4 +- ...KustomizeEditSetSecretOptions.Generated.cs | 4 +- .../KustomizeLocalizeOptions.Generated.cs | 12 + .../Services/IKustomizeCfg.Generated.cs | 7 +- .../Services/IKustomizeEdit.Generated.cs | 3 + .../Services/IKustomizeFn.Generated.cs | 3 + .../Services/KustomizeCfg.Generated.cs | 8 +- .../Services/KustomizeEditAdd.Generated.cs | 27 +- .../Services/KustomizeEditRemove.Generated.cs | 27 +- .../Services/KustomizeEditSet.Generated.cs | 27 +- .../Options/PulumiDestroyOptions.Generated.cs | 11 +- .../Options/PulumiEnvDiffOptions.Generated.cs | 9 +- .../Options/PulumiEnvEditOptions.Generated.cs | 3 +- ...nvProviderAwsLoginOidcOptions.Generated.cs | 9 +- ...ProviderAwsLoginStaticOptions.Generated.cs | 9 +- ...ProviderAzureLoginOidcOptions.Generated.cs | 9 +- ...oviderAzureLoginStaticOptions.Generated.cs | 9 +- ...nvProviderGcpLoginOidcOptions.Generated.cs | 9 +- ...ProviderGcpLoginStaticOptions.Generated.cs | 9 +- .../Options/PulumiEnvSetOptions.Generated.cs | 3 +- .../Options/PulumiImportOptions.Generated.cs | 3 +- .../PulumiPackageAddOptions.Generated.cs | 6 + .../PulumiPackageGenSdkOptions.Generated.cs | 6 + ...ulumiPackageGetMappingOptions.Generated.cs | 6 + ...PulumiPackageGetSchemaOptions.Generated.cs | 6 + .../PulumiPackageInfoOptions.Generated.cs | 6 + .../PulumiPackagePublishOptions.Generated.cs | 6 + .../PulumiPluginInstallOptions.Generated.cs | 16 +- .../Options/PulumiPreviewOptions.Generated.cs | 13 +- .../Options/PulumiRefreshOptions.Generated.cs | 3 +- .../PulumiStackNewOptions.Generated.cs | 10 +- .../Options/PulumiUpOptions.Generated.cs | 13 +- .../Services/IPulumiStack.Generated.cs | 2 +- .../Services/PulumiStack.Generated.cs | 4 +- .../Options/CargoAddOptions.Generated.cs | 2 +- .../Options/SkopeoCopyOptions.Generated.cs | 3 +- .../Options/SkopeoDeleteOptions.Generated.cs | 2 +- ...peoGenerateSigstoreKeyOptions.Generated.cs | 3 +- .../Options/SkopeoInspectOptions.Generated.cs | 2 +- .../SkopeoListTagsOptions.Generated.cs | 2 +- .../Options/SkopeoLoginOptions.Generated.cs | 2 +- .../Options/SkopeoLogoutOptions.Generated.cs | 2 +- .../SkopeoStandaloneSignOptions.Generated.cs | 3 +- .../Options/SkopeoSyncOptions.Generated.cs | 3 +- .../Generators/GeneratorHardeningTests.cs | 196 ++++++++- .../Scrapers/DockerCliCompatibilityTests.cs | 47 ++ .../Generators/CodeGeneratorOrchestrator.cs | 9 +- .../GeneratedApiCompatibilityPreserver.cs | 401 +++++++++++++++--- .../Generators/OptionsClassGenerator.cs | 17 + .../Generators/SubDomainClassGenerator.cs | 4 + .../Models/CliCommandDefinition.cs | 31 ++ .../Scrapers/Cli/CobraCliScraper.cs | 16 +- 63 files changed, 922 insertions(+), 177 deletions(-) diff --git a/src/ModularPipelines.Buildah/Options/BuildahFromOptions.Generated.cs b/src/ModularPipelines.Buildah/Options/BuildahFromOptions.Generated.cs index 15d720b6b7c..8f00a441032 100644 --- a/src/ModularPipelines.Buildah/Options/BuildahFromOptions.Generated.cs +++ b/src/ModularPipelines.Buildah/Options/BuildahFromOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Buildah.Options; +using ModularPipelines.Models; namespace ModularPipelines.Buildah.Options; @@ -221,8 +222,8 @@ public record BuildahFromOptions : BuildahOptions /// /// pull the image from the registry if newer or not present in store, if false, only pull the image if not present, if always, pull the image even if the named image is present in store, if never, only use the image present in store if available (default "true") /// - [CliOption("--pull", Format = OptionFormat.EqualsSeparated)] - public string? Pull { get; set; } + [CliOption("--pull", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] + public CliOptionValue? Pull { get; set; } /// /// don't output progress information when pulling images diff --git a/src/ModularPipelines.Flux/Options/FluxPluginInstallOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxPluginInstallOptions.Generated.cs index 7499b0c760a..949498f29d2 100644 --- a/src/ModularPipelines.Flux/Options/FluxPluginInstallOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxPluginInstallOptions.Generated.cs @@ -19,7 +19,7 @@ namespace ModularPipelines.Flux.Options; [ExcludeFromCodeCoverage] [CliSubCommand("plugin", "install")] public record FluxPluginInstallOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string NameVersion + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name ) : FluxOptions { /// diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgGrepOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgGrepOptions.Generated.cs index 2fc15e404bc..8d2f823ca94 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeCfgGrepOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeCfgGrepOptions.Generated.cs @@ -18,7 +18,9 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("cfg", "grep")] -public record KustomizeCfgGrepOptions : KustomizeOptions +public record KustomizeCfgGrepOptions( + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Query +) : KustomizeOptions { /// /// annotate resources with their file origins. (default true) @@ -50,7 +52,10 @@ public record KustomizeCfgGrepOptions : KustomizeOptions [CliFlag("--stack-trace")] public bool? StackTrace { get; set; } - [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + /// + /// The DIR operand. + /// + [CliArgument(1, Phase = CommandLinePhase.EarlyOperand)] public string? Dir { get; set; } } diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeCreateOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeCreateOptions.Generated.cs index 4bd9891d29b..8ba62b4f394 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeCreateOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeCreateOptions.Generated.cs @@ -24,7 +24,7 @@ public record KustomizeCreateOptions : KustomizeOptions /// Add one or more common annotations. /// [CliOption("--annotations", Format = OptionFormat.EqualsSeparated)] - public string? Annotations { get; set; } + public IEnumerable? Annotations { get; set; } /// /// Search for kubernetes resources in the current directory to be added to the kustomization file. @@ -42,7 +42,7 @@ public record KustomizeCreateOptions : KustomizeOptions /// Add one or more common labels. /// [CliOption("--labels", Format = OptionFormat.EqualsSeparated)] - public string? Labels { get; set; } + public IEnumerable? Labels { get; set; } /// /// Sets the value of the namePrefix field in the kustomization file. diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddBuildmetadataOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddBuildmetadataOptions.Generated.cs index 0f16d610867..bb1c98f9906 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddBuildmetadataOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddBuildmetadataOptions.Generated.cs @@ -18,7 +18,9 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "add", "buildmetadata")] -public record KustomizeEditAddBuildmetadataOptions : KustomizeOptions +public record KustomizeEditAddBuildmetadataOptions( + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Metadata +) : KustomizeOptions { /// /// help for buildmetadata diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddConfigmapOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddConfigmapOptions.Generated.cs index fe6499370d5..b3cfa61083a 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddConfigmapOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddConfigmapOptions.Generated.cs @@ -18,7 +18,9 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "add", "configmap")] -public record KustomizeEditAddConfigmapOptions : KustomizeOptions +public record KustomizeEditAddConfigmapOptions( + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name +) : KustomizeOptions { /// /// Specify the behavior for config map generation, i.e whether to create a new configmap (the default), to merge with a previously defined one, or to replace an existing one. Merge and replace should be used only when overriding an existing configmap defined in a base @@ -30,7 +32,7 @@ public record KustomizeEditAddConfigmapOptions : KustomizeOptions /// Disable the name suffix for the configmap /// [CliFlag("--disableNameSuffixHash")] - public bool? Disablenamesuffixhash { get; set; } + public bool? DisableNameSuffixHash { get; set; } /// /// Specify the path to a file to read lines of key=val pairs to create a configmap (i.e. a Docker .env file). diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddSecretOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddSecretOptions.Generated.cs index 71042c2be0d..ca6c4392c63 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddSecretOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditAddSecretOptions.Generated.cs @@ -18,13 +18,15 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "add", "secret")] -public record KustomizeEditAddSecretOptions : KustomizeOptions +public record KustomizeEditAddSecretOptions( + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name +) : KustomizeOptions { /// /// Disable the name suffix for the secret /// [CliFlag("--disableNameSuffixHash")] - public bool? Disablenamesuffixhash { get; set; } + public bool? DisableNameSuffixHash { get; set; } /// /// Specify the path to a file to read lines of key=val pairs to create a secret (i.e. a Docker .env file). diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveBuildmetadataOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveBuildmetadataOptions.Generated.cs index 50f99feb0a5..65bc499db19 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveBuildmetadataOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveBuildmetadataOptions.Generated.cs @@ -18,7 +18,9 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "remove", "buildmetadata")] -public record KustomizeEditRemoveBuildmetadataOptions : KustomizeOptions +public record KustomizeEditRemoveBuildmetadataOptions( + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Metadata +) : KustomizeOptions { /// /// help for buildmetadata diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveConfigmapOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveConfigmapOptions.Generated.cs index 7e9b10d523d..a1ae4d6b6d1 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveConfigmapOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveConfigmapOptions.Generated.cs @@ -18,7 +18,9 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "remove", "configmap")] -public record KustomizeEditRemoveConfigmapOptions : KustomizeOptions +public record KustomizeEditRemoveConfigmapOptions( + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name +) : KustomizeOptions { /// /// help for configmap diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveSecretOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveSecretOptions.Generated.cs index 8ea6c123297..b458f32b30f 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveSecretOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditRemoveSecretOptions.Generated.cs @@ -18,7 +18,9 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "remove", "secret")] -public record KustomizeEditRemoveSecretOptions : KustomizeOptions +public record KustomizeEditRemoveSecretOptions( + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name +) : KustomizeOptions { /// /// help for secret diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetBuildmetadataOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetBuildmetadataOptions.Generated.cs index 6c32d372d1c..f05c2fb6435 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetBuildmetadataOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetBuildmetadataOptions.Generated.cs @@ -18,7 +18,9 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "set", "buildmetadata")] -public record KustomizeEditSetBuildmetadataOptions : KustomizeOptions +public record KustomizeEditSetBuildmetadataOptions( + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Metadata +) : KustomizeOptions { /// /// help for buildmetadata diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetConfigmapOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetConfigmapOptions.Generated.cs index 71eda5e8ad9..5d9fafbfb95 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetConfigmapOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetConfigmapOptions.Generated.cs @@ -18,7 +18,9 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "set", "configmap")] -public record KustomizeEditSetConfigmapOptions : KustomizeOptions +public record KustomizeEditSetConfigmapOptions( + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name +) : KustomizeOptions { /// /// Specify an existing key and a new value to update a ConfigMap (i.e. mykey=newvalue) diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetSecretOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetSecretOptions.Generated.cs index ff3167c1e32..733da974ede 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetSecretOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeEditSetSecretOptions.Generated.cs @@ -18,7 +18,9 @@ namespace ModularPipelines.Kubernetes.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("edit", "set", "secret")] -public record KustomizeEditSetSecretOptions : KustomizeOptions +public record KustomizeEditSetSecretOptions( + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string Name +) : KustomizeOptions { /// /// Specify an existing key and a new value to update a Secret (i.e. mykey=newvalue) diff --git a/src/ModularPipelines.Kubernetes/Options/KustomizeLocalizeOptions.Generated.cs b/src/ModularPipelines.Kubernetes/Options/KustomizeLocalizeOptions.Generated.cs index 610aeaa2d3c..8ae89a58818 100644 --- a/src/ModularPipelines.Kubernetes/Options/KustomizeLocalizeOptions.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Options/KustomizeLocalizeOptions.Generated.cs @@ -44,4 +44,16 @@ public record KustomizeLocalizeOptions : KustomizeOptions [CliFlag("--stack-trace")] public bool? StackTrace { get; set; } + /// + /// The target operand. + /// + [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] + public string? Target { get; set; } + + /// + /// The destination operand. + /// + [CliArgument(1, Phase = CommandLinePhase.EarlyOperand)] + public string? Destination { get; set; } + } diff --git a/src/ModularPipelines.Kubernetes/Services/IKustomizeCfg.Generated.cs b/src/ModularPipelines.Kubernetes/Services/IKustomizeCfg.Generated.cs index 0948a61def2..8aa6299c3b5 100644 --- a/src/ModularPipelines.Kubernetes/Services/IKustomizeCfg.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/IKustomizeCfg.Generated.cs @@ -15,6 +15,9 @@ namespace ModularPipelines.Kubernetes.Services; /// /// kustomize cfg commands. /// +/// +/// Nested sub-command groups are exposed as concrete services; only this top-level facade is interface-backed. +/// [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] public interface IKustomizeCfg { @@ -35,7 +38,7 @@ Task ExecuteAsync(KustomizeCfgOptions? options = null, CommandExe /// The execution configuration options. /// Cancellation token. /// The command result. - Task CatAsync(KustomizeCfgCatOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) + Task CatAsync(KustomizeCfgCatOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) => throw new System.NotSupportedException(); /// @@ -55,7 +58,7 @@ Task CountAsync(KustomizeCfgCountOptions? options = null, Command /// The execution configuration options. /// Cancellation token. /// The command result. - Task GrepAsync(KustomizeCfgGrepOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) + Task GrepAsync(KustomizeCfgGrepOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) => throw new System.NotSupportedException(); /// diff --git a/src/ModularPipelines.Kubernetes/Services/IKustomizeEdit.Generated.cs b/src/ModularPipelines.Kubernetes/Services/IKustomizeEdit.Generated.cs index 48dc6d14be8..f608755445a 100644 --- a/src/ModularPipelines.Kubernetes/Services/IKustomizeEdit.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/IKustomizeEdit.Generated.cs @@ -15,6 +15,9 @@ namespace ModularPipelines.Kubernetes.Services; /// /// kustomize edit commands. /// +/// +/// Nested sub-command groups are exposed as concrete services; only this top-level facade is interface-backed. +/// [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] public interface IKustomizeEdit { diff --git a/src/ModularPipelines.Kubernetes/Services/IKustomizeFn.Generated.cs b/src/ModularPipelines.Kubernetes/Services/IKustomizeFn.Generated.cs index 3b4a2c4f411..63ee52e5a9c 100644 --- a/src/ModularPipelines.Kubernetes/Services/IKustomizeFn.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/IKustomizeFn.Generated.cs @@ -15,6 +15,9 @@ namespace ModularPipelines.Kubernetes.Services; /// /// kustomize fn commands. /// +/// +/// Nested sub-command groups are exposed as concrete services; only this top-level facade is interface-backed. +/// [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] public interface IKustomizeFn { diff --git a/src/ModularPipelines.Kubernetes/Services/KustomizeCfg.Generated.cs b/src/ModularPipelines.Kubernetes/Services/KustomizeCfg.Generated.cs index 6ffc8d66972..3bd92ade885 100644 --- a/src/ModularPipelines.Kubernetes/Services/KustomizeCfg.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/KustomizeCfg.Generated.cs @@ -54,11 +54,11 @@ public virtual async Task ExecuteAsync( /// Cancellation token. /// The command result. public virtual async Task CatAsync( - KustomizeCfgCatOptions? options = null, + KustomizeCfgCatOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeCfgCatOptions(), executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); } /// @@ -84,11 +84,11 @@ public virtual async Task CountAsync( /// Cancellation token. /// The command result. public virtual async Task GrepAsync( - KustomizeCfgGrepOptions? options = null, + KustomizeCfgGrepOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeCfgGrepOptions(), executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); } /// diff --git a/src/ModularPipelines.Kubernetes/Services/KustomizeEditAdd.Generated.cs b/src/ModularPipelines.Kubernetes/Services/KustomizeEditAdd.Generated.cs index 928b77a5e27..7f073493880 100644 --- a/src/ModularPipelines.Kubernetes/Services/KustomizeEditAdd.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/KustomizeEditAdd.Generated.cs @@ -31,6 +31,21 @@ public KustomizeEditAdd(ICommandContext command) #region Commands + /// + /// Adds an item to the kustomization file + /// + /// The command options. + /// The execution configuration options. + /// Cancellation token. + /// The command result. + public virtual async Task ExecuteAsync( + KustomizeEditAddOptions? options = null, + CommandExecutionOptions? executionOptions = null, + CancellationToken cancellationToken = default) + { + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditAddOptions(), executionOptions, cancellationToken); + } + /// /// Adds one or more commonAnnotations to kustomization.yaml /// @@ -69,11 +84,11 @@ public virtual async Task BaseAsync( /// Cancellation token. /// The command result. public virtual async Task BuildmetadataAsync( - KustomizeEditAddBuildmetadataOptions? options = null, + KustomizeEditAddBuildmetadataOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditAddBuildmetadataOptions(), executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); } /// @@ -99,11 +114,11 @@ public virtual async Task ComponentAsync( /// Cancellation token. /// The command result. public virtual async Task ConfigmapAsync( - KustomizeEditAddConfigmapOptions? options = null, + KustomizeEditAddConfigmapOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditAddConfigmapOptions(), executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); } /// @@ -174,11 +189,11 @@ public virtual async Task ResourceAsync( /// Cancellation token. /// The command result. public virtual async Task SecretAsync( - KustomizeEditAddSecretOptions? options = null, + KustomizeEditAddSecretOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditAddSecretOptions(), executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); } /// diff --git a/src/ModularPipelines.Kubernetes/Services/KustomizeEditRemove.Generated.cs b/src/ModularPipelines.Kubernetes/Services/KustomizeEditRemove.Generated.cs index 1e3f92e735b..b905d35db0f 100644 --- a/src/ModularPipelines.Kubernetes/Services/KustomizeEditRemove.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/KustomizeEditRemove.Generated.cs @@ -31,6 +31,21 @@ public KustomizeEditRemove(ICommandContext command) #region Commands + /// + /// Removes items from the kustomization file + /// + /// The command options. + /// The execution configuration options. + /// Cancellation token. + /// The command result. + public virtual async Task ExecuteAsync( + KustomizeEditRemoveOptions? options = null, + CommandExecutionOptions? executionOptions = null, + CancellationToken cancellationToken = default) + { + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditRemoveOptions(), executionOptions, cancellationToken); + } + /// /// Removes one or more commonAnnotations from kustomization.yaml /// @@ -54,11 +69,11 @@ public virtual async Task AnnotationAsync( /// Cancellation token. /// The command result. public virtual async Task BuildmetadataAsync( - KustomizeEditRemoveBuildmetadataOptions? options = null, + KustomizeEditRemoveBuildmetadataOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditRemoveBuildmetadataOptions(), executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); } /// @@ -69,11 +84,11 @@ public virtual async Task BuildmetadataAsync( /// Cancellation token. /// The command result. public virtual async Task ConfigmapAsync( - KustomizeEditRemoveConfigmapOptions? options = null, + KustomizeEditRemoveConfigmapOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditRemoveConfigmapOptions(), executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); } /// @@ -129,11 +144,11 @@ public virtual async Task ResourceAsync( /// Cancellation token. /// The command result. public virtual async Task SecretAsync( - KustomizeEditRemoveSecretOptions? options = null, + KustomizeEditRemoveSecretOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditRemoveSecretOptions(), executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); } /// diff --git a/src/ModularPipelines.Kubernetes/Services/KustomizeEditSet.Generated.cs b/src/ModularPipelines.Kubernetes/Services/KustomizeEditSet.Generated.cs index faf6c60dcfa..abe46f89af0 100644 --- a/src/ModularPipelines.Kubernetes/Services/KustomizeEditSet.Generated.cs +++ b/src/ModularPipelines.Kubernetes/Services/KustomizeEditSet.Generated.cs @@ -31,6 +31,21 @@ public KustomizeEditSet(ICommandContext command) #region Commands + /// + /// Sets the value of different fields in kustomization file + /// + /// The command options. + /// The execution configuration options. + /// Cancellation token. + /// The command result. + public virtual async Task ExecuteAsync( + KustomizeEditSetOptions? options = null, + CommandExecutionOptions? executionOptions = null, + CancellationToken cancellationToken = default) + { + return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditSetOptions(), executionOptions, cancellationToken); + } + /// /// Sets one or more commonAnnotations in kustomization.yaml /// @@ -54,11 +69,11 @@ public virtual async Task AnnotationAsync( /// Cancellation token. /// The command result. public virtual async Task BuildmetadataAsync( - KustomizeEditSetBuildmetadataOptions? options = null, + KustomizeEditSetBuildmetadataOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditSetBuildmetadataOptions(), executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); } /// @@ -69,11 +84,11 @@ public virtual async Task BuildmetadataAsync( /// Cancellation token. /// The command result. public virtual async Task ConfigmapAsync( - KustomizeEditSetConfigmapOptions? options = null, + KustomizeEditSetConfigmapOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditSetConfigmapOptions(), executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); } /// @@ -174,11 +189,11 @@ public virtual async Task ReplicasAsync( /// Cancellation token. /// The command result. public virtual async Task SecretAsync( - KustomizeEditSetSecretOptions? options = null, + KustomizeEditSetSecretOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options ?? new KustomizeEditSetSecretOptions(), executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); } #endregion diff --git a/src/ModularPipelines.Pulumi/Options/PulumiDestroyOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiDestroyOptions.Generated.cs index da50fa574bb..42144e71142 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiDestroyOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiDestroyOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; +using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -74,6 +75,12 @@ public record PulumiDestroyOptions : PulumiOptions [CliFlag("--help", ShortForm = "-h")] public bool? Help { get; set; } + /// + /// Ignore the protect resource option for this operation, allowing protected resources to be destroyed. Use with caution: deleted resources cannot be recovered + /// + [CliFlag("--ignore-protect")] + public bool? IgnoreProtect { get; set; } + /// /// Serialize the destroy diffs, operations, and overall output as JSON /// @@ -114,7 +121,7 @@ public record PulumiDestroyOptions : PulumiOptions /// Refresh the state of the stack's resources before this update /// [CliOption("--refresh", ShortForm = "-r", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? Refresh { get; set; } + public CliOptionValue? Refresh { get; set; } /// /// Remove the stack and its config file after all resources in the stack have been deleted @@ -186,7 +193,7 @@ public record PulumiDestroyOptions : PulumiOptions /// Suppress display of the state permalink /// [CliOption("--suppress-permalink", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? SuppressPermalink { get; set; } + public CliOptionValue? SuppressPermalink { get; set; } /// /// Suppress display of periodic progress dots diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvDiffOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvDiffOptions.Generated.cs index 4e918a044e7..65b12be8a53 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvDiffOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvDiffOptions.Generated.cs @@ -19,7 +19,8 @@ namespace ModularPipelines.Pulumi.Options; [ExcludeFromCodeCoverage] [CliSubCommand("env", "diff")] public record PulumiEnvDiffOptions( - [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string EnvironmentName + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string EnvironmentName, + [property: CliArgument(1, Phase = CommandLinePhase.EarlyOperand, Required = true)] string OrgNameOrProjectNameOrEnvironmentNameVersion ) : PulumiOptions { /// @@ -130,10 +131,4 @@ public record PulumiEnvDiffOptions( [CliOption("--verbose", ShortForm = "-v", Format = OptionFormat.EqualsSeparated)] public int? Verbose { get; set; } - /// - /// The [[org-name Or ][<project-name> Or ]<environment-name>]@<version> operand. - /// - [CliArgument(1, Phase = CommandLinePhase.EarlyOperand)] - public string? OrgNameOrProjectNameOrEnvironmentNameVersion { get; set; } - } diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvEditOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvEditOptions.Generated.cs index 39921251013..f2ec554e248 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvEditOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvEditOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; +using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -26,7 +27,7 @@ public record PulumiEnvEditOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? Draft { get; set; } + public CliOptionValue? Draft { get; set; } /// /// the command to use to edit the environment definition diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginOidcOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginOidcOptions.Generated.cs index bcc0787b71f..938a84ef1cf 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginOidcOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginOidcOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; +using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -34,7 +35,7 @@ public record PulumiEnvProviderAwsLoginOidcOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? Draft { get; set; } + public CliOptionValue? Draft { get; set; } /// /// optional session duration, e.g. 1h @@ -42,6 +43,12 @@ public record PulumiEnvProviderAwsLoginOidcOptions( [CliOption("--duration", Format = OptionFormat.EqualsSeparated)] public string? Duration { get; set; } + /// + /// also set the AWS SDK environment variables (AWS_ACCESS_KEY_ID, etc.) referencing the login outputs + /// + [CliFlag("--export-env-vars")] + public bool? ExportEnvVars { get; set; } + /// /// help for oidc /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginStaticOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginStaticOptions.Generated.cs index a77694c1d6d..e35041f1e9f 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginStaticOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAwsLoginStaticOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; +using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -34,7 +35,13 @@ public record PulumiEnvProviderAwsLoginStaticOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? Draft { get; set; } + public CliOptionValue? Draft { get; set; } + + /// + /// also set the AWS SDK environment variables (AWS_ACCESS_KEY_ID, etc.) referencing the login outputs + /// + [CliFlag("--export-env-vars")] + public bool? ExportEnvVars { get; set; } /// /// help for static diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginOidcOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginOidcOptions.Generated.cs index 7a7fc0934d6..c10e899759e 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginOidcOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginOidcOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; +using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -35,7 +36,13 @@ public record PulumiEnvProviderAzureLoginOidcOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? Draft { get; set; } + public CliOptionValue? Draft { get; set; } + + /// + /// also set the Azure environment variables (ARM_CLIENT_ID, etc.) referencing the login outputs + /// + [CliFlag("--export-env-vars")] + public bool? ExportEnvVars { get; set; } /// /// help for oidc diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginStaticOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginStaticOptions.Generated.cs index dac3783ebef..eb7cfc919b2 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginStaticOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderAzureLoginStaticOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; +using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -36,7 +37,13 @@ public record PulumiEnvProviderAzureLoginStaticOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? Draft { get; set; } + public CliOptionValue? Draft { get; set; } + + /// + /// also set the Azure environment variables (ARM_CLIENT_ID, etc.) referencing the login outputs + /// + [CliFlag("--export-env-vars")] + public bool? ExportEnvVars { get; set; } /// /// help for static diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginOidcOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginOidcOptions.Generated.cs index ad9223ddc7c..835e2e5cb5c 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginOidcOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginOidcOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; +using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -33,7 +34,13 @@ public record PulumiEnvProviderGcpLoginOidcOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? Draft { get; set; } + public CliOptionValue? Draft { get; set; } + + /// + /// also set the Google SDK environment variables (GOOGLE_PROJECT, etc.) referencing the login outputs + /// + [CliFlag("--export-env-vars")] + public bool? ExportEnvVars { get; set; } /// /// help for oidc diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginStaticOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginStaticOptions.Generated.cs index fd6621de3f2..8cd3b484cdd 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginStaticOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvProviderGcpLoginStaticOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; +using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -34,7 +35,13 @@ public record PulumiEnvProviderGcpLoginStaticOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? Draft { get; set; } + public CliOptionValue? Draft { get; set; } + + /// + /// also set the Google SDK environment variables (GOOGLE_PROJECT, etc.) referencing the login outputs + /// + [CliFlag("--export-env-vars")] + public bool? ExportEnvVars { get; set; } /// /// help for static diff --git a/src/ModularPipelines.Pulumi/Options/PulumiEnvSetOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiEnvSetOptions.Generated.cs index 9ebdd4eefaa..d7e0972c591 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiEnvSetOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiEnvSetOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; +using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -28,7 +29,7 @@ public record PulumiEnvSetOptions( /// set flag without a value (--draft) to create a draft rather than saving changes directly. --draft=<change-request-id> to update an existing change request. /// [CliOption("--draft", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? Draft { get; set; } + public CliOptionValue? Draft { get; set; } /// /// If set, the value is read from the specified file. Pass - to read from standard input. diff --git a/src/ModularPipelines.Pulumi/Options/PulumiImportOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiImportOptions.Generated.cs index 9317765bdd0..42b2124e296 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiImportOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiImportOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; +using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -150,7 +151,7 @@ public record PulumiImportOptions : PulumiOptions /// Suppress display of the state permalink /// [CliOption("--suppress-permalink", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? SuppressPermalink { get; set; } + public CliOptionValue? SuppressPermalink { get; set; } /// /// Suppress display of periodic progress dots diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPackageAddOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPackageAddOptions.Generated.cs index 54e25872d80..b0e4fa39836 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPackageAddOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPackageAddOptions.Generated.cs @@ -40,6 +40,12 @@ public record PulumiPackageAddOptions( [CliOption("--language", Format = OptionFormat.EqualsSeparated)] public string? Language { get; set; } + /// + /// A URL to download the plugin from. When set, the provider argument is used as the plugin name directly and no package resolution is performed. + /// + [CliOption("--server", Format = OptionFormat.EqualsSeparated)] + public string? Server { get; set; } + /// /// Colorize output. Choices are: always, never, raw, auto (default "auto") /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPackageGenSdkOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPackageGenSdkOptions.Generated.cs index 1b39cef8f71..b07430e7570 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPackageGenSdkOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPackageGenSdkOptions.Generated.cs @@ -52,6 +52,12 @@ public record PulumiPackageGenSdkOptions( [CliOption("--out", ShortForm = "-o", Format = OptionFormat.EqualsSeparated)] public string? Out { get; set; } + /// + /// A URL to download the plugin from. When set, the provider argument is used as the plugin name directly and no package resolution is performed. + /// + [CliOption("--server", Format = OptionFormat.EqualsSeparated)] + public string? Server { get; set; } + /// /// The provider plugin version to generate the SDK for /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPackageGetMappingOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPackageGetMappingOptions.Generated.cs index bccae586c92..a1efc6f2767 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPackageGetMappingOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPackageGetMappingOptions.Generated.cs @@ -35,6 +35,12 @@ public record PulumiPackageGetMappingOptions( [CliOption("--out", ShortForm = "-o", Format = OptionFormat.EqualsSeparated)] public string? Out { get; set; } + /// + /// A URL to download the plugin from. When set, the provider argument is used as the plugin name directly and no package resolution is performed. + /// + [CliOption("--server", Format = OptionFormat.EqualsSeparated)] + public string? Server { get; set; } + /// /// Colorize output. Choices are: always, never, raw, auto (default "auto") /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPackageGetSchemaOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPackageGetSchemaOptions.Generated.cs index 0e3c69a9151..611b34b8fce 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPackageGetSchemaOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPackageGetSchemaOptions.Generated.cs @@ -34,6 +34,12 @@ public record PulumiPackageGetSchemaOptions( [CliFlag("--help", ShortForm = "-h")] public bool? Help { get; set; } + /// + /// A URL to download the plugin from. When set, the provider argument is used as the plugin name directly and no package resolution is performed. + /// + [CliOption("--server", Format = OptionFormat.EqualsSeparated)] + public string? Server { get; set; } + /// /// Colorize output. Choices are: always, never, raw, auto (default "auto") /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPackageInfoOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPackageInfoOptions.Generated.cs index 7eeb1564a65..b591c7f4ba3 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPackageInfoOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPackageInfoOptions.Generated.cs @@ -52,6 +52,12 @@ public record PulumiPackageInfoOptions( [CliOption("--resource", ShortForm = "-r", Format = OptionFormat.EqualsSeparated)] public string? Resource { get; set; } + /// + /// A URL to download the plugin from. When set, the provider argument is used as the plugin name directly and no package resolution is performed. + /// + [CliOption("--server", Format = OptionFormat.EqualsSeparated)] + public string? Server { get; set; } + /// /// Colorize output. Choices are: always, never, raw, auto (default "auto") /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPackagePublishOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPackagePublishOptions.Generated.cs index 67070d23606..9da2c35b9bb 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPackagePublishOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPackagePublishOptions.Generated.cs @@ -46,6 +46,12 @@ public record PulumiPackagePublishOptions( [CliOption("--readme", Format = OptionFormat.EqualsSeparated)] public string? Readme { get; set; } + /// + /// A URL to download the plugin from. When set, the provider argument is used as the plugin name directly and no package resolution is performed. + /// + [CliOption("--server", Format = OptionFormat.EqualsSeparated)] + public string? Server { get; set; } + /// /// Colorize output. Choices are: always, never, raw, auto (default "auto") /// diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPluginInstallOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPluginInstallOptions.Generated.cs index 83db231c873..937a6f3ac6a 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPluginInstallOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPluginInstallOptions.Generated.cs @@ -135,9 +135,21 @@ public record PulumiPluginInstallOptions : PulumiOptions public int? Verbose { get; set; } /// - /// The kind name [version] operand. + /// The kind operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public string? KindNameVersion { get; set; } + public string? Kind { get; set; } + + /// + /// The name operand. + /// + [CliArgument(1, Phase = CommandLinePhase.EarlyOperand)] + public string? Name { get; set; } + + /// + /// The version operand. + /// + [CliArgument(2, Phase = CommandLinePhase.EarlyOperand)] + public string? Version { get; set; } } diff --git a/src/ModularPipelines.Pulumi/Options/PulumiPreviewOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiPreviewOptions.Generated.cs index 7bb3de30b87..80dfe8d0955 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiPreviewOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiPreviewOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; +using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -24,7 +25,7 @@ public record PulumiPreviewOptions : PulumiOptions /// Enable the ability to attach a debugger to the program and source based plugins being executed. Can limit debug type to 'program', 'plugins', 'plugin:<name>' or 'all'. /// [CliOption("--attach-debugger", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public IEnumerable? AttachDebugger { get; set; } + public IEnumerable? AttachDebugger { get; set; } /// /// Config to use during the preview and save to the stack config file @@ -80,6 +81,12 @@ public record PulumiPreviewOptions : PulumiOptions [CliFlag("--help", ShortForm = "-h")] public bool? Help { get; set; } + /// + /// Ignore the protect resource option for this operation, previewing the deletion or replacement of protected resources instead of failing + /// + [CliFlag("--ignore-protect")] + public bool? IgnoreProtect { get; set; } + /// /// Save any creates seen during the preview into an import file to use with 'pulumi import' /// @@ -132,7 +139,7 @@ public record PulumiPreviewOptions : PulumiOptions /// Refresh the state of the stack's resources before this update /// [CliOption("--refresh", ShortForm = "-r", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? Refresh { get; set; } + public CliOptionValue? Refresh { get; set; } /// /// Specify resources to replace. Multiple resources can be specified using --replace urn1 --replace urn2 @@ -223,7 +230,7 @@ public record PulumiPreviewOptions : PulumiOptions /// Suppress display of the state permalink /// [CliOption("--suppress-permalink", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? SuppressPermalink { get; set; } + public CliOptionValue? SuppressPermalink { get; set; } /// /// Suppress display of periodic progress dots diff --git a/src/ModularPipelines.Pulumi/Options/PulumiRefreshOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiRefreshOptions.Generated.cs index a6342e9aae8..b7fbe7784d6 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiRefreshOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiRefreshOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; +using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -180,7 +181,7 @@ public record PulumiRefreshOptions : PulumiOptions /// Suppress display of the state permalink /// [CliOption("--suppress-permalink", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? SuppressPermalink { get; set; } + public CliOptionValue? SuppressPermalink { get; set; } /// /// Suppress display of periodic progress dots diff --git a/src/ModularPipelines.Pulumi/Options/PulumiStackNewOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiStackNewOptions.Generated.cs index e343bbde426..da6483c4614 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiStackNewOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiStackNewOptions.Generated.cs @@ -18,7 +18,9 @@ namespace ModularPipelines.Pulumi.Options; [GeneratedCode("ModularPipelines.OptionsGenerator", "2.0.0")] [ExcludeFromCodeCoverage] [CliSubCommand("stack", "new")] -public record PulumiStackNewOptions : PulumiOptions +public record PulumiStackNewOptions( + [property: CliArgument(0, Phase = CommandLinePhase.EarlyOperand, Required = true)] string OrgNameOrStackName +) : PulumiOptions { /// /// The name of the stack to copy existing config from @@ -140,10 +142,4 @@ public record PulumiStackNewOptions : PulumiOptions [CliOption("--verbose", ShortForm = "-v", Format = OptionFormat.EqualsSeparated)] public int? Verbose { get; set; } - /// - /// The [org-name Or ]<stack-name> operand. - /// - [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public string? OrgNameOrStackName { get; set; } - } diff --git a/src/ModularPipelines.Pulumi/Options/PulumiUpOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiUpOptions.Generated.cs index 15469a750fd..cf058a50234 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiUpOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiUpOptions.Generated.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using ModularPipelines.Attributes; using ModularPipelines.Pulumi.Options; +using ModularPipelines.Models; namespace ModularPipelines.Pulumi.Options; @@ -24,7 +25,7 @@ public record PulumiUpOptions : PulumiOptions /// Enable the ability to attach a debugger to the program and source based plugins being executed. Can limit debug type to 'program', 'plugins', 'plugin:<name>' or 'all'. /// [CliOption("--attach-debugger", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public IEnumerable? AttachDebugger { get; set; } + public IEnumerable? AttachDebugger { get; set; } /// /// Config to use during the update and save to the stack config file @@ -86,6 +87,12 @@ public record PulumiUpOptions : PulumiOptions [CliFlag("--help", ShortForm = "-h")] public bool? Help { get; set; } + /// + /// Ignore the protect resource option for this operation, allowing protected resources to be deleted or replaced. Use with caution: deleted resources cannot be recovered + /// + [CliFlag("--ignore-protect")] + public bool? IgnoreProtect { get; set; } + /// /// Serialize the update diffs, operations, and overall output as JSON /// @@ -132,7 +139,7 @@ public record PulumiUpOptions : PulumiOptions /// Refresh the state of the stack's resources before this update /// [CliOption("--refresh", ShortForm = "-r", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? Refresh { get; set; } + public CliOptionValue? Refresh { get; set; } /// /// Specify a single resource URN to replace. Multiple resources can be specified using --replace urn1 --replace urn2. Wildcards (*, **) are also supported @@ -234,7 +241,7 @@ public record PulumiUpOptions : PulumiOptions /// Suppress display of the state permalink /// [CliOption("--suppress-permalink", Format = OptionFormat.EqualsSeparated, ValueArity = CliOptionValueArity.Optional)] - public string? SuppressPermalink { get; set; } + public CliOptionValue? SuppressPermalink { get; set; } /// /// Suppress display of periodic progress dots diff --git a/src/ModularPipelines.Pulumi/Services/IPulumiStack.Generated.cs b/src/ModularPipelines.Pulumi/Services/IPulumiStack.Generated.cs index 6b13a6ef0f3..bb08e3299a4 100644 --- a/src/ModularPipelines.Pulumi/Services/IPulumiStack.Generated.cs +++ b/src/ModularPipelines.Pulumi/Services/IPulumiStack.Generated.cs @@ -125,7 +125,7 @@ public interface IPulumiStack /// The execution configuration options. /// Cancellation token. /// The command result. - Task NewAsync(PulumiStackNewOptions? options = null, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default); + Task NewAsync(PulumiStackNewOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default); /// /// Show a stack's output properties. diff --git a/src/ModularPipelines.Pulumi/Services/PulumiStack.Generated.cs b/src/ModularPipelines.Pulumi/Services/PulumiStack.Generated.cs index e763c78e873..1b55fd5dd60 100644 --- a/src/ModularPipelines.Pulumi/Services/PulumiStack.Generated.cs +++ b/src/ModularPipelines.Pulumi/Services/PulumiStack.Generated.cs @@ -193,11 +193,11 @@ public virtual async Task MigrateAsync( /// Cancellation token. /// The command result. public virtual async Task NewAsync( - PulumiStackNewOptions? options = null, + PulumiStackNewOptions options, CommandExecutionOptions? executionOptions = null, CancellationToken cancellationToken = default) { - return await _command.ExecuteCommandLineToolAsync(options ?? new PulumiStackNewOptions(), executionOptions, cancellationToken); + return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken); } /// diff --git a/src/ModularPipelines.Rust/Options/CargoAddOptions.Generated.cs b/src/ModularPipelines.Rust/Options/CargoAddOptions.Generated.cs index 479d78f1579..1b261faed45 100644 --- a/src/ModularPipelines.Rust/Options/CargoAddOptions.Generated.cs +++ b/src/ModularPipelines.Rust/Options/CargoAddOptions.Generated.cs @@ -20,7 +20,7 @@ namespace ModularPipelines.Rust.Options; [ExcludeFromCodeCoverage] [CliSubCommand("add")] public record CargoAddOptions( - [property: CliArgument(0, Phase = CommandLinePhase.Passthrough, Required = true)] IEnumerable DepVersion + [property: CliArgument(0, Phase = CommandLinePhase.Passthrough, Required = true)] IEnumerable Dep ) : CargoOptions { } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoCopyOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoCopyOptions.Generated.cs index 3178894522b..80fef9f52cd 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoCopyOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoCopyOptions.Generated.cs @@ -234,7 +234,6 @@ public record SkopeoCopyOptions( /// /// Read a passphrase for signing an image from PATH /// - [SecretValue] [CliOption("--sign-passphrase-file", Format = OptionFormat.EqualsSeparated)] public string? SignPassphraseFile { get; set; } @@ -304,6 +303,6 @@ public record SkopeoCopyOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public string? CommandOptions { get; set; } + public IEnumerable? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoDeleteOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoDeleteOptions.Generated.cs index 24016a740fc..b66b9e9c0e7 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoDeleteOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoDeleteOptions.Generated.cs @@ -100,6 +100,6 @@ public record SkopeoDeleteOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public string? CommandOptions { get; set; } + public IEnumerable? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoGenerateSigstoreKeyOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoGenerateSigstoreKeyOptions.Generated.cs index dbfff0b2e05..a4fd7ca4405 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoGenerateSigstoreKeyOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoGenerateSigstoreKeyOptions.Generated.cs @@ -37,7 +37,6 @@ public record SkopeoGenerateSigstoreKeyOptions( /// /// Read a passphrase for the private key from PATH /// - [SecretValue] [CliOption("--passphrase-file", Format = OptionFormat.EqualsSeparated)] public string? PassphraseFile { get; set; } @@ -45,6 +44,6 @@ public record SkopeoGenerateSigstoreKeyOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public string? CommandOptions { get; set; } + public IEnumerable? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoInspectOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoInspectOptions.Generated.cs index 9d4cf4c7dcf..e5887e0ce61 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoInspectOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoInspectOptions.Generated.cs @@ -124,6 +124,6 @@ public record SkopeoInspectOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public string? CommandOptions { get; set; } + public IEnumerable? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoListTagsOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoListTagsOptions.Generated.cs index 93e322250a8..e266ef7a56f 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoListTagsOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoListTagsOptions.Generated.cs @@ -88,6 +88,6 @@ public record SkopeoListTagsOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public string? CommandOptions { get; set; } + public IEnumerable? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoLoginOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoLoginOptions.Generated.cs index 00b81777250..4c355e92314 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoLoginOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoLoginOptions.Generated.cs @@ -87,6 +87,6 @@ public record SkopeoLoginOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public string? CommandOptions { get; set; } + public IEnumerable? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoLogoutOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoLogoutOptions.Generated.cs index dad3ee3deec..9d11a6e29d5 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoLogoutOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoLogoutOptions.Generated.cs @@ -56,6 +56,6 @@ public record SkopeoLogoutOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public string? CommandOptions { get; set; } + public IEnumerable? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoStandaloneSignOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoStandaloneSignOptions.Generated.cs index 343afad0123..228be82744a 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoStandaloneSignOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoStandaloneSignOptions.Generated.cs @@ -40,7 +40,6 @@ public record SkopeoStandaloneSignOptions( /// /// file that contains a passphrase for the --sign-by key /// - [SecretValue] [CliOption("--passphrase-file", Format = OptionFormat.EqualsSeparated)] public string? PassphraseFile { get; set; } @@ -48,6 +47,6 @@ public record SkopeoStandaloneSignOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public string? CommandOptions { get; set; } + public IEnumerable? CommandOptions { get; set; } } diff --git a/src/ModularPipelines.Skopeo/Options/SkopeoSyncOptions.Generated.cs b/src/ModularPipelines.Skopeo/Options/SkopeoSyncOptions.Generated.cs index 9ba28e06cd1..49365bd9cc9 100644 --- a/src/ModularPipelines.Skopeo/Options/SkopeoSyncOptions.Generated.cs +++ b/src/ModularPipelines.Skopeo/Options/SkopeoSyncOptions.Generated.cs @@ -169,7 +169,6 @@ public record SkopeoSyncOptions( /// /// File that contains a passphrase for the --sign-by key /// - [SecretValue] [CliOption("--sign-passphrase-file", Format = OptionFormat.EqualsSeparated)] public string? SignPassphraseFile { get; set; } @@ -233,6 +232,6 @@ public record SkopeoSyncOptions( /// The command options operand. /// [CliArgument(0, Phase = CommandLinePhase.EarlyOperand)] - public string? CommandOptions { get; set; } + public IEnumerable? CommandOptions { get; set; } } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs index ebbdc4201bd..e15c9675b3c 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs @@ -959,7 +959,7 @@ await Assert.That(exception.Message) } [Test] - public async Task ApiCompatibilityPreserver_Rejects_Optional_To_Required_Changes() + public async Task ApiCompatibilityPreserver_Preserves_Old_Constructor_When_Member_Becomes_Required() { var command = Command("ToolNewOptions", "ToolOptions", ["new"]) with { @@ -975,12 +975,49 @@ public async Task ApiCompatibilityPreserver_Rejects_Optional_To_Required_Changes ], }; + var preserved = GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("Name", "string", argumentPosition: 0)]); + var generated = (await new OptionsClassGenerator().GenerateAsync(Tool(preserved))).Single().Content; + + await Assert.That(generated).Contains("public ToolNewOptions()"); + await Assert.That(generated).Contains(": this(default!)"); + } + + [Test] + public async Task ApiCompatibilityPreserver_Restores_Required_Member_Demoted_To_Optional() + { + var command = Command("ToolDiffOptions", "ToolOptions", ["diff"]) with + { + PositionalArguments = + [ + new CliPositionalArgument + { + PropertyName = "Target", + CSharpType = "string?", + PositionIndex = 0, + }, + ], + }; + + var preserved = GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("Target", "string", argumentPosition: 0, isRequired: true)]); + var generated = (await new OptionsClassGenerator().GenerateAsync(Tool(preserved))).Single().Content; + + await Assert.That(generated).Contains("string Target"); + await Assert.That(generated).DoesNotContain("public string? Target"); + } + + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Removed_Positional_Operands() + { var exception = Assert.Throws(() => GeneratedApiCompatibilityPreserver.Preserve( - command, - [BaselineProperty("Name", "string", argumentPosition: 0)])); + Command("ToolInstallOptions", "ToolOptions", ["install"]), + [BaselineProperty("Name", "string?", argumentPosition: 1)])); - await Assert.That(exception.Message).Contains("ToolNewOptions.Name changed from optional to required"); + await Assert.That(exception.Message).Contains("Name positional argument was removed"); } [Test] @@ -996,11 +1033,11 @@ public async Task ApiCompatibilityPreserver_Rejects_Removed_Required_Members() isRequired: true)])); await Assert.That(exception.Message) - .Contains("ToolAddOptions.Package was removed from the required constructor"); + .Contains("ToolAddOptions.Package positional argument was removed"); } [Test] - public async Task ApiCompatibilityPreserver_Rejects_Added_Required_Members() + public async Task ApiCompatibilityPreserver_Preserves_Parameterless_Constructor_When_Required_Member_Is_Added() { var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with { @@ -1016,11 +1053,152 @@ public async Task ApiCompatibilityPreserver_Rejects_Added_Required_Members() ], }; + var preserved = GeneratedApiCompatibilityPreserver.Preserve(command, []); + var generated = (await new OptionsClassGenerator().GenerateAsync(Tool(preserved))).Single().Content; + + await Assert.That(generated).Contains("public ToolAddOptions()"); + await Assert.That(generated).Contains(": this(default!)"); + } + + [Test] + public async Task ApiCompatibilityPreserver_Retains_Previously_Generated_Secondary_Constructors() + { + var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}"); + var optionsDirectory = Path.Combine(root, "src", "ModularPipelines.Tool", "Options"); + Directory.CreateDirectory(optionsDirectory); + try + { + await File.WriteAllTextAsync( + Path.Combine(optionsDirectory, "ToolAddOptions.Generated.cs"), + "public record ToolAddOptions([property: CliArgument(0)] string Package) " + + "{ public ToolAddOptions() : this(default!) { } }"); + var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with + { + PositionalArguments = + [ + new CliPositionalArgument + { + PropertyName = "Package", + CSharpType = "string", + IsRequired = true, + PositionIndex = 0, + }, + ], + }; + + var preserved = GeneratedApiCompatibilityPreserver.Preserve(Tool(command), root); + var generated = (await new OptionsClassGenerator().GenerateAsync(preserved)).Single().Content; + + await Assert.That(generated).Contains("public ToolAddOptions()"); + await Assert.That(generated).Contains(": this(default!)"); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + + [Test] + public async Task ApiCompatibilityPreserver_Retains_Command_Group_Execute_Facades() + { + var root = Path.Combine(Path.GetTempPath(), $"service-api-{Guid.NewGuid():N}"); + var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool"); + Directory.CreateDirectory(Path.Combine(packageDirectory, "Options")); + Directory.CreateDirectory(Path.Combine(packageDirectory, "Services")); + try + { + await File.WriteAllTextAsync( + Path.Combine(packageDirectory, "Options", "ToolEditAddOptions.Generated.cs"), + "public record ToolEditAddOptions;"); + await File.WriteAllTextAsync( + Path.Combine(packageDirectory, "Services", "ToolEditAdd.Generated.cs"), + "public class ToolEditAdd { public Task ExecuteAsync(ToolEditAddOptions? options = null) => Task.CompletedTask; }"); + var parent = Command( + "ToolEditAddOptions", + "ToolOptions", + ["edit", "add"], + subDomainGroup: "edit"); + var child = Command( + "ToolEditAddSecretOptions", + "ToolOptions", + ["edit", "add", "secret"], + subDomainGroup: "edit"); + + var preserved = GeneratedApiCompatibilityPreserver.Preserve(Tool(parent, child), root); + var generated = (await new SubDomainClassGenerator().GenerateAsync(preserved)) + .Single(file => file.RelativePath.EndsWith( + "ToolEditAdd.Generated.cs", + StringComparison.Ordinal)) + .Content; + + await Assert.That(generated).Contains("Task ExecuteAsync("); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + + [Test] + public async Task ApiCompatibilityPreserver_Rekeys_Documentation_Examples_After_Required_Rename() + { + var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with + { + PositionalArguments = + [ + new CliPositionalArgument + { + PropertyName = "CurrentName", + CSharpType = "string", + IsRequired = true, + PositionIndex = 0, + }, + ], + DocumentationExampleValues = new Dictionary(StringComparer.Ordinal) + { + ["CurrentName"] = "\"example\"", + }, + }; + + var preserved = GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("StableName", "string", argumentPosition: 0, isRequired: true)]); + + await Assert.That(preserved.DocumentationExampleValues.Keys).IsEquivalentTo(["StableName"]); + } + + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Required_Rename_Collisions() + { + var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with + { + PositionalArguments = + [ + new CliPositionalArgument + { + PropertyName = "CurrentName", + CSharpType = "string", + IsRequired = true, + PositionIndex = 0, + }, + ], + Options = + [ + new CliOptionDefinition + { + SwitchName = "--stable-name", + PropertyName = "StableName", + CSharpType = "string?", + }, + ], + }; + var exception = Assert.Throws(() => - GeneratedApiCompatibilityPreserver.Preserve(command, [])); + GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("StableName", "string", argumentPosition: 0, isRequired: true)])); - await Assert.That(exception.Message) - .Contains("ToolAddOptions.Package was added to the required constructor"); + await Assert.That(exception.Message).Contains("would duplicate a member name"); } [Test] diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs index c22ead1ce61..d950762a9b6 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs @@ -48,6 +48,24 @@ Execute a command in a running container await Assert.That(option.ShortForm).IsEqualTo("-T"); } + [Test] + public async Task Switch_Normalization_Rejects_Distinct_Options_With_One_Canonical_Name() + { + const string helpText = """ + Usage: fake run [OPTIONS] + + Options: + --current string Current value + --legacy string Legacy value + """; + + await Assert.That(() => new CollidingSwitchScraper().Parse( + ["fake", "run"], + helpText)) + .Throws() + .And.HasMessageContaining("maps both '--current' and '--legacy'"); + } + private static CliCommandDefinition CreateCommand(string commandGroup) { return new CliCommandDefinition @@ -77,4 +95,33 @@ public TestDockerCliScraper() return ParseCommandAsync(commandPath, helpText, usage, CancellationToken.None); } } + + private sealed class CollidingSwitchScraper : CobraCliScraper + { + public CollidingSwitchScraper() + : base( + new ProcessCliCommandExecutor(NullLogger.Instance), + new HelpTextCache(NullLogger.Instance), + NullLogger.Instance) + { + } + + public override string ToolName => "fake"; + + public override string NamespacePrefix => "Fake"; + + public override string TargetNamespace => "ModularPipelines.Fake"; + + public override string OutputDirectory => "src/ModularPipelines.Fake"; + + protected override string NormalizeOptionSwitchName( + string[] commandParts, + string switchName) => "--canonical"; + + public Task Parse(string[] commandPath, string helpText) + { + var usage = ParseUsageSynopsis(commandPath, helpText); + return ParseCommandAsync(commandPath, helpText, usage, CancellationToken.None); + } + } } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/CodeGeneratorOrchestrator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/CodeGeneratorOrchestrator.cs index b84390a1968..70cf4a1cb9a 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/CodeGeneratorOrchestrator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/CodeGeneratorOrchestrator.cs @@ -834,16 +834,17 @@ private async Task GenerateForToolAsync( { var globalOptions = tool.GetGlobalOptions(); var normalizedCommands = GeneratorUtils.NormalizeCommandClassNames(tool.Commands); - var compatibleTool = GeneratedApiCompatibilityPreserver.Preserve( + var collisionResolvedTool = InheritedPropertyCollisionResolver.Resolve( tool with { Commands = normalizedCommands, GlobalOptions = globalOptions, SupplementalGlobalOptions = [], - }, + }); + var compatibleTool = GeneratedApiCompatibilityPreserver.Preserve( + collisionResolvedTool, outputDirectory); - var toolDefinition = InheritedPropertyCollisionResolver.Resolve( - ExecutablePrerequisiteCatalog.PrepareForGeneration(compatibleTool)); + var toolDefinition = ExecutablePrerequisiteCatalog.PrepareForGeneration(compatibleTool); if (enforceOutputContainment) { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index 0302f57e908..ef008df7f1a 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -7,6 +7,13 @@ namespace ModularPipelines.OptionsGenerator.Generators; internal static class GeneratedApiCompatibilityPreserver { + private enum RequiredMemberRestoreResult + { + NotFound, + Restored, + Rejected, + } + public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDirectory) { var optionsDirectory = Path.Combine( @@ -19,14 +26,19 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi } var baseline = ReadBaseline(optionsDirectory); - var compatibleTool = baseline.TryGetValue($"{tool.NamespacePrefix}Options", out var globalProperties) - ? PreserveGlobalOptions(tool, globalProperties) + var compatibleTool = baseline.TryGetValue($"{tool.NamespacePrefix}Options", out var globalBaseline) + ? PreserveGlobalOptions(tool, globalBaseline.Properties) : tool; + var executeFacadeOptionTypes = ReadExecuteFacadeOptionTypes( + Path.Combine(outputDirectory, tool.OutputDirectory, "Services")); return compatibleTool with { Commands = compatibleTool.Commands - .Select(command => baseline.TryGetValue(command.ClassName, out var properties) - ? Preserve(command, properties) + .Select(command => baseline.TryGetValue(command.ClassName, out var commandBaseline) + ? Preserve(command, commandBaseline.Properties, commandBaseline.Constructors) + : command) + .Select(command => executeFacadeOptionTypes.Contains(command.ClassName) + ? command with { PreserveExecuteFacade = true } : command) .ToArray(), }; @@ -60,20 +72,30 @@ internal static CliToolDefinition PreserveGlobalOptions( internal static CliCommandDefinition Preserve( CliCommandDefinition command, - IReadOnlyList baselineProperties) + IReadOnlyList baselineProperties) => + Preserve(command, baselineProperties, []); + + private static CliCommandDefinition Preserve( + CliCommandDefinition command, + IReadOnlyList baselineProperties, + IReadOnlyList baselineConstructors) { var compatibilityProperties = command.CompatibilityProperties.ToList(); + var compatibilityConstructors = command.CompatibilityConstructors.ToList(); var positionalArguments = command.PositionalArguments.ToArray(); var options = command.Options.ToArray(); + var violations = new List(); + var renamedProperties = new Dictionary(StringComparer.Ordinal); RestoreRequiredMemberNames( baselineProperties, positionalArguments, options, - compatibilityProperties); + compatibilityProperties, + renamedProperties, + violations); var currentProperties = GetCurrentProperties(positionalArguments, options); - var violations = new List(); foreach (var baseline in baselineProperties) { PreserveBaselineProperty( @@ -84,7 +106,12 @@ internal static CliCommandDefinition Preserve( violations); } - AddNewRequiredMemberViolations(command, baselineProperties, currentProperties, violations); + PreserveCompatibilityConstructors( + baselineProperties, + baselineConstructors, + positionalArguments, + options, + compatibilityConstructors); if (violations.Count > 0) { @@ -99,6 +126,10 @@ internal static CliCommandDefinition Preserve( Options = options, PositionalArguments = positionalArguments, CompatibilityProperties = compatibilityProperties, + CompatibilityConstructors = compatibilityConstructors, + DocumentationExampleValues = RenameDocumentationExampleValues( + command.DocumentationExampleValues, + renamedProperties), }; } @@ -135,6 +166,13 @@ private static void PreserveBaselineProperty( return; } + if (baseline.ArgumentPosition is not null && replacement is null) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} positional argument was removed"); + return; + } + if (baseline.IsRequired && replacement is null) { violations.Add( @@ -181,10 +219,10 @@ private static void ValidateMatchingProperty( $"{command.ClassName}.{baseline.PropertyName} changed type from " + $"{baseline.CSharpType} to {current.CSharpType}"); } - else if (!baseline.IsRequired && current.IsRequired) + else if (baseline.IsRequired && !current.IsRequired) { violations.Add( - $"{command.ClassName}.{baseline.PropertyName} changed from optional to required"); + $"{command.ClassName}.{baseline.PropertyName} changed from required to optional"); } else if (!HasSameCliIdentity(current, baseline)) { @@ -193,78 +231,144 @@ private static void ValidateMatchingProperty( } } - private static void AddNewRequiredMemberViolations( - CliCommandDefinition command, - IReadOnlyList baselineProperties, - IEnumerable currentProperties, - List violations) - { - foreach (var current in currentProperties.Where(static property => property.IsRequired)) - { - var existedInBaseline = baselineProperties.Any(baseline => - !baseline.IsCompatibility - && baseline.PropertyName.Equals(current.PropertyName, StringComparison.Ordinal)); - if (!existedInBaseline) - { - violations.Add( - $"{command.ClassName}.{current.PropertyName} was added to the required constructor"); - } - } - } - private static void RestoreRequiredMemberNames( IReadOnlyList baselineProperties, CliPositionalArgument[] positionalArguments, CliOptionDefinition[] options, - ICollection compatibilityProperties) + ICollection compatibilityProperties, + IDictionary renamedProperties, + ICollection violations) { foreach (var baseline in baselineProperties.Where(property => property.IsRequired && !property.IsCompatibility)) { - var positionalIndex = Array.FindIndex(positionalArguments, current => - current.IsRequired - && HasSameCliIdentity(ToGeneratedProperty(current), baseline) - && ToGeneratedProperty(current).CSharpType.Equals( - baseline.CSharpType, - StringComparison.Ordinal)); - if (positionalIndex >= 0 - && !positionalArguments[positionalIndex].PropertyName.Equals( - baseline.PropertyName, - StringComparison.Ordinal)) - { - var currentName = positionalArguments[positionalIndex].PropertyName; - positionalArguments[positionalIndex] = positionalArguments[positionalIndex] with + var propertyNames = positionalArguments.Select(argument => argument.PropertyName) + .Concat(options.Select(option => option.PropertyName)); + var positionalResult = TryRestoreRequiredMember( + baseline, + positionalArguments, + propertyNames, + ToGeneratedProperty, + static argument => argument.CSharpType, + argument => argument with { PropertyName = baseline.PropertyName, - }; - AddRenamedCurrentProperty( - compatibilityProperties, - currentName, - baseline.CSharpType, - baseline.PropertyName); + CSharpType = baseline.CSharpType, + IsRequired = true, + }, + violations, + out var currentName); + if (positionalResult != RequiredMemberRestoreResult.NotFound) + { + if (positionalResult == RequiredMemberRestoreResult.Restored) + { + RecordRequiredMemberRename( + compatibilityProperties, + renamedProperties, + currentName, + baseline); + } + continue; } - var optionIndex = Array.FindIndex(options, current => - current.IsRequired - && HasSameCliIdentity(ToGeneratedProperty(current), baseline) - && ToGeneratedProperty(current).CSharpType.Equals( - baseline.CSharpType, - StringComparison.Ordinal)); - if (optionIndex < 0 - || options[optionIndex].PropertyName.Equals(baseline.PropertyName, StringComparison.Ordinal)) + var optionResult = TryRestoreRequiredMember( + baseline, + options, + propertyNames, + ToGeneratedProperty, + static option => option.PropertyType, + option => option with + { + PropertyName = baseline.PropertyName, + CSharpType = baseline.CSharpType, + IsRequired = true, + }, + violations, + out currentName); + if (optionResult == RequiredMemberRestoreResult.Restored) { - continue; + RecordRequiredMemberRename( + compatibilityProperties, + renamedProperties, + currentName, + baseline); } + } + } - var optionName = options[optionIndex].PropertyName; - options[optionIndex] = options[optionIndex] with { PropertyName = baseline.PropertyName }; - AddRenamedCurrentProperty( - compatibilityProperties, - optionName, - baseline.CSharpType, - baseline.PropertyName); + private static RequiredMemberRestoreResult TryRestoreRequiredMember( + GeneratedApiProperty baseline, + T[] members, + IEnumerable propertyNames, + Func toGeneratedProperty, + Func getEmittedType, + Func restore, + ICollection violations, + out string currentName) + { + var index = Array.FindIndex(members, current => + HasSameCliIdentity(toGeneratedProperty(current), baseline) + && getEmittedType(current).TrimEnd('?').Equals( + baseline.CSharpType.TrimEnd('?'), + StringComparison.Ordinal)); + if (index < 0) + { + currentName = string.Empty; + return RequiredMemberRestoreResult.NotFound; + } + + currentName = toGeneratedProperty(members[index]).PropertyName; + if (!CanRestoreName( + baseline.PropertyName, + currentName, + propertyNames, + violations)) + { + return RequiredMemberRestoreResult.Rejected; + } + + members[index] = restore(members[index]); + return RequiredMemberRestoreResult.Restored; + } + + private static bool CanRestoreName( + string baselineName, + string currentName, + IEnumerable propertyNames, + ICollection violations) + { + if (baselineName.Equals(currentName, StringComparison.Ordinal)) + { + return true; + } + + if (!propertyNames.Any(name => name.Equals(baselineName, StringComparison.Ordinal))) + { + return true; + } + + violations.Add($"restoring required member {currentName} to {baselineName} would duplicate a member name"); + return false; + } + + private static void RecordRequiredMemberRename( + ICollection compatibilityProperties, + IDictionary renamedProperties, + string currentName, + GeneratedApiProperty baseline) + { + if (currentName.Equals(baseline.PropertyName, StringComparison.Ordinal)) + { + return; } + + renamedProperties[currentName] = baseline.PropertyName; + AddRenamedCurrentProperty( + compatibilityProperties, + currentName, + baseline.CSharpType, + baseline.PropertyName); } private static void AddRenamedCurrentProperty( @@ -295,6 +399,110 @@ private static void AddCompatibilityProperty( compatibilityProperties.Add(property); } + private static void PreserveCompatibilityConstructors( + IReadOnlyList baselineProperties, + IReadOnlyList baselineConstructors, + IReadOnlyList positionalArguments, + IReadOnlyList options, + List compatibilityConstructors) + { + var currentRequired = GetCurrentProperties(positionalArguments, options) + .Where(static property => property.IsRequired) + .ToArray(); + if (currentRequired.Length == 0) + { + compatibilityConstructors.Clear(); + return; + } + + foreach (var constructor in baselineConstructors) + { + AddCompatibilityConstructor(compatibilityConstructors, constructor, currentRequired); + } + + var baselineRequired = baselineProperties + .Where(static property => property.IsRequired && !property.IsCompatibility) + .ToArray(); + if (HasSameConstructorSignature(baselineRequired, currentRequired)) + { + return; + } + + var baselineParameters = baselineRequired + .Select(property => new CliCompatibilityConstructorParameter( + property.PropertyName, + property.CSharpType)) + .ToArray(); + var primaryArguments = currentRequired + .Select(current => baselineRequired.Any(baseline => + baseline.PropertyName.Equals(current.PropertyName, StringComparison.Ordinal) + && baseline.CSharpType.Equals(current.CSharpType, StringComparison.Ordinal)) + ? current.PropertyName + : "default!") + .ToArray(); + AddCompatibilityConstructor( + compatibilityConstructors, + new CliCompatibilityConstructor + { + Parameters = baselineParameters, + PrimaryConstructorArguments = primaryArguments, + }, + currentRequired); + } + + private static void AddCompatibilityConstructor( + ICollection constructors, + CliCompatibilityConstructor constructor, + IReadOnlyList currentRequired) + { + if (HasSameConstructorSignature(constructor.Parameters, currentRequired) + || constructors.Any(existing => HasSameConstructorSignature( + existing.Parameters, + constructor.Parameters))) + { + return; + } + + constructors.Add(constructor); + } + + private static bool HasSameConstructorSignature( + IReadOnlyList left, + IReadOnlyList right) + where TLeft : notnull + where TRight : notnull + { + if (left.Count != right.Count) + { + return false; + } + + return left.Select(GetConstructorParameterType) + .SequenceEqual(right.Select(GetConstructorParameterType), StringComparer.Ordinal); + } + + private static string GetConstructorParameterType(T parameter) => parameter switch + { + GeneratedApiProperty property => property.CSharpType, + CliCompatibilityConstructorParameter compatibilityParameter => compatibilityParameter.CSharpType, + _ => throw new ArgumentOutOfRangeException(nameof(parameter)), + }; + + private static IReadOnlyDictionary RenameDocumentationExampleValues( + IReadOnlyDictionary values, + Dictionary renamedProperties) + { + if (renamedProperties.Count == 0) + { + return values; + } + + return values.ToDictionary( + pair => renamedProperties.GetValueOrDefault(pair.Key, pair.Key), + pair => pair.Value, + StringComparer.Ordinal); + } + private static GeneratedApiProperty[] GetCurrentProperties( IEnumerable positionalArguments, IEnumerable options) => @@ -341,10 +549,10 @@ private static bool HasSameCliIdentity( return true; } - private static Dictionary> ReadBaseline( + private static Dictionary ReadBaseline( string optionsDirectory) { - var baseline = new Dictionary>(StringComparer.Ordinal); + var baseline = new Dictionary(StringComparer.Ordinal); foreach (var path in Directory.EnumerateFiles( optionsDirectory, "*.Generated.cs", @@ -353,13 +561,66 @@ private static Dictionary> ReadBasel var root = CSharpSyntaxTree.ParseText(File.ReadAllText(path)).GetRoot(); foreach (var declaration in root.DescendantNodes().OfType()) { - baseline[declaration.Identifier.ValueText] = ReadProperties(declaration); + baseline[declaration.Identifier.ValueText] = new GeneratedApiBaseline( + ReadProperties(declaration), + ReadCompatibilityConstructors(declaration)); } } return baseline; } + private static CliCompatibilityConstructor[] ReadCompatibilityConstructors( + RecordDeclarationSyntax declaration) => + declaration.Members + .OfType() + .Where(constructor => constructor.Modifiers.Any(SyntaxKind.PublicKeyword)) + .Where(constructor => constructor.Initializer?.IsKind( + SyntaxKind.ThisConstructorInitializer) == true) + .Select(constructor => new CliCompatibilityConstructor + { + Parameters = constructor.ParameterList.Parameters + .Select(parameter => new CliCompatibilityConstructorParameter( + parameter.Identifier.ValueText, + parameter.Type?.ToString() ?? string.Empty)) + .ToArray(), + PrimaryConstructorArguments = constructor.Initializer!.ArgumentList.Arguments + .Select(argument => argument.Expression.ToString()) + .ToArray(), + }) + .ToArray(); + + private static HashSet ReadExecuteFacadeOptionTypes(string servicesDirectory) + { + var optionTypes = new HashSet(StringComparer.Ordinal); + if (!Directory.Exists(servicesDirectory)) + { + return optionTypes; + } + + foreach (var path in Directory.EnumerateFiles( + servicesDirectory, + "*.Generated.cs", + SearchOption.TopDirectoryOnly)) + { + var root = CSharpSyntaxTree.ParseText(File.ReadAllText(path)).GetRoot(); + foreach (var method in root.DescendantNodes().OfType() + .Where(method => method.Identifier.ValueText.Equals( + "ExecuteAsync", + StringComparison.Ordinal)) + .Where(method => method.Modifiers.Any(SyntaxKind.PublicKeyword))) + { + var optionsType = method.ParameterList.Parameters.FirstOrDefault()?.Type?.ToString(); + if (!string.IsNullOrWhiteSpace(optionsType)) + { + optionTypes.Add(optionsType.TrimEnd('?')); + } + } + } + + return optionTypes; + } + private static List ReadProperties( RecordDeclarationSyntax declaration) { @@ -448,3 +709,7 @@ internal sealed record GeneratedApiProperty( bool IsCompatibility, string? ForwardToPropertyName, string? ObsoleteMessage); + +internal sealed record GeneratedApiBaseline( + IReadOnlyList Properties, + IReadOnlyList Constructors); diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs index 6c503a856bb..9bd6e642f5a 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs @@ -262,12 +262,29 @@ private static string GenerateOptionsClass(CliCommandDefinition command, CliTool var existingPropertyNames = GenerateClassDeclaration(sb, command, positionalArguments); sb.AppendLine("{"); + GenerateCompatibilityConstructors(sb, command); GenerateProperties(sb, command, positionalArguments, existingPropertyNames); sb.AppendLine("}"); return sb.ToString(); } + private static void GenerateCompatibilityConstructors( + StringBuilder sb, + CliCommandDefinition command) + { + foreach (var constructor in command.CompatibilityConstructors) + { + var parameters = constructor.Parameters + .Select(parameter => $"{parameter.CSharpType} {parameter.PropertyName}"); + sb.AppendLine($" public {command.ClassName}({string.Join(", ", parameters)})"); + sb.AppendLine($" : this({string.Join(", ", constructor.PrimaryConstructorArguments)})"); + sb.AppendLine(" {"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + } + private static void GenerateUsings(StringBuilder sb, CliCommandDefinition command, CliToolDefinition tool) { sb.AppendLine("using System.CodeDom.Compiler;"); diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs index 22ed87fe3a6..7d7467264c0 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs @@ -81,6 +81,10 @@ private static void GenerateFilesFromTree( IReadOnlyList commandGroupAliases, CliCommandDefinition? parentCommand = null) { + parentCommand ??= tool.Commands.FirstOrDefault(command => + command.PreserveExecuteFacade + && command.ClassName.Equals($"{node.ClassName}Options", StringComparison.Ordinal)); + // Build map of commands that collide with child property names // These will become ExecuteAsync() methods on the child classes instead var collidingCommands = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs index f40daf3406c..f15fa3fe3a5 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs @@ -92,6 +92,16 @@ public record CliCommandDefinition /// public IReadOnlyList CompatibilityProperties { get; init; } = []; + /// + /// Secondary constructors retained when the CLI adds required members to the primary constructor. + /// + public IReadOnlyList CompatibilityConstructors { get; init; } = []; + + /// + /// Whether an existing command-group ExecuteAsync facade must remain generated. + /// + public bool PreserveExecuteFacade { get; init; } + /// /// Public methods retained as obsolete forwarding aliases for compatibility. /// @@ -186,6 +196,27 @@ public record CliCompatibilityProperty public required string ObsoleteMessage { get; init; } } +/// +/// Describes a secondary generated constructor retained for source compatibility. +/// +public record CliCompatibilityConstructor +{ + /// + /// Parameters exposed by the retained constructor. + /// + public required IReadOnlyList Parameters { get; init; } + + /// + /// Arguments forwarded to the current primary constructor. + /// + public required IReadOnlyList PrimaryConstructorArguments { get; init; } +} + +/// +/// Describes a parameter on a retained generated constructor. +/// +public readonly record struct CliCompatibilityConstructorParameter(string PropertyName, string CSharpType); + /// /// Describes an obsolete generated method retained as a forwarding alias. /// diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs index f8996770e23..1c1098e7589 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs @@ -264,7 +264,7 @@ protected override IEnumerable ExtractSubcommands(string helpText) private List ParseOptions(string helpText, string[] commandParts) { var options = new List(); - var seenOptions = new HashSet(StringComparer.OrdinalIgnoreCase); + var seenOptions = new Dictionary(StringComparer.OrdinalIgnoreCase); var className = GenerateClassName([ToolName, .. commandParts]); // Find Flags, Options, and Global Flags sections @@ -312,15 +312,25 @@ private List ParseOptions(string helpText, string[] command continue; } + var scrapedLongForm = longForm; longForm = NormalizeOptionSwitchName(commandParts, longForm); // Skip duplicates - if (seenOptions.Contains(longForm)) + if (seenOptions.TryGetValue(longForm, out var existingScrapedLongForm)) { + if (!existingScrapedLongForm.Equals( + scrapedLongForm, + StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + $"Option switch normalization for '{string.Join(' ', commandParts)}' " + + $"maps both '{existingScrapedLongForm}' and '{scrapedLongForm}' to '{longForm}'."); + } + continue; } - seenOptions.Add(longForm); + seenOptions.Add(longForm, scrapedLongForm); var propertyName = NormalizePropertyName(longForm); if (propertyName is null) From 0d3d21263fc9474b048472373f346a2579c2f134 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:58:41 +0100 Subject: [PATCH 07/21] fix(generator): preserve record API shapes --- .../Generators/GeneratorHardeningTests.cs | 124 ++++++++++++++- .../GeneratedApiCompatibilityPreserver.cs | 147 +++++++++++++++--- .../Generators/OptionsClassGenerator.cs | 15 ++ .../Models/CliCommandDefinition.cs | 5 + 4 files changed, 269 insertions(+), 22 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs index e15c9675b3c..1a33f533bd5 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs @@ -116,6 +116,15 @@ private static GeneratedApiProperty BaselineProperty( null, null); + private static CliOptionDefinition RequiredOption(string switchName, string propertyName) => + new() + { + SwitchName = switchName, + PropertyName = propertyName, + CSharpType = "string", + IsRequired = true, + }; + [Test] public async Task Command_Facade_Generation_Can_Be_Disabled_Independently_Of_Options() { @@ -959,7 +968,7 @@ await Assert.That(exception.Message) } [Test] - public async Task ApiCompatibilityPreserver_Preserves_Old_Constructor_When_Member_Becomes_Required() + public async Task ApiCompatibilityPreserver_Rejects_Optional_Member_Becoming_Required() { var command = Command("ToolNewOptions", "ToolOptions", ["new"]) with { @@ -975,13 +984,85 @@ public async Task ApiCompatibilityPreserver_Preserves_Old_Constructor_When_Membe ], }; + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("Name", "string", argumentPosition: 0)])); + + await Assert.That(exception.Message) + .Contains("Name changed from optional to required and would remove its public setter"); + } + + [Test] + public async Task ApiCompatibilityPreserver_Restores_Required_Constructor_Order() + { + var command = Command("ToolMoveOptions", "ToolOptions", ["move"]) with + { + Options = + [ + new CliOptionDefinition + { + SwitchName = "--second", + PropertyName = "Second", + CSharpType = "string", + IsRequired = true, + }, + new CliOptionDefinition + { + SwitchName = "--first", + PropertyName = "First", + CSharpType = "string", + IsRequired = true, + }, + ], + }; + var preserved = GeneratedApiCompatibilityPreserver.Preserve( command, - [BaselineProperty("Name", "string", argumentPosition: 0)]); + [ + BaselineProperty("First", "string", switchName: "--first", isRequired: true), + BaselineProperty("Second", "string", switchName: "--second", isRequired: true), + ]); var generated = (await new OptionsClassGenerator().GenerateAsync(Tool(preserved))).Single().Content; - await Assert.That(generated).Contains("public ToolNewOptions()"); - await Assert.That(generated).Contains(": this(default!)"); + await Assert.That(generated.IndexOf("string First", StringComparison.Ordinal)) + .IsLessThan(generated.IndexOf("string Second", StringComparison.Ordinal)); + } + + [Test] + public async Task ApiCompatibilityPreserver_Preserves_Old_Deconstruct_Arity() + { + var command = Command("ToolMoveOptions", "ToolOptions", ["move"]) with + { + Options = + [ + new CliOptionDefinition + { + SwitchName = "--source", + PropertyName = "Source", + CSharpType = "string", + IsRequired = true, + }, + new CliOptionDefinition + { + SwitchName = "--destination", + PropertyName = "Destination", + CSharpType = "string", + IsRequired = true, + }, + ], + }; + + var preserved = GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("Source", "string", switchName: "--source", isRequired: true)]); + var generated = (await new OptionsClassGenerator().GenerateAsync(Tool(preserved))).Single().Content; + + using (Assert.Multiple()) + { + await Assert.That(generated).Contains("public void Deconstruct(out string Source)"); + await Assert.That(generated).Contains("Source = this.Source;"); + } } [Test] @@ -1098,6 +1179,41 @@ await File.WriteAllTextAsync( } } + [Test] + public async Task ApiCompatibilityPreserver_Retains_Previously_Generated_Deconstruct_Overloads() + { + var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}"); + var optionsDirectory = Path.Combine(root, "src", "ModularPipelines.Tool", "Options"); + Directory.CreateDirectory(optionsDirectory); + try + { + await File.WriteAllTextAsync( + Path.Combine(optionsDirectory, "ToolMoveOptions.Generated.cs"), + "public record ToolMoveOptions([property: CliOption(\"--source\")] string Source, " + + "[property: CliOption(\"--destination\")] string Destination) " + + "{ public ToolMoveOptions(string Source) : this(Source, default!) { } " + + "public void Deconstruct(out string Source) { Source = this.Source; } }"); + var command = Command("ToolMoveOptions", "ToolOptions", ["move"]) with + { + Options = + [ + RequiredOption("--source", "Source"), + RequiredOption("--destination", "Destination"), + RequiredOption("--force", "Force"), + ], + }; + + var preserved = GeneratedApiCompatibilityPreserver.Preserve(Tool(command), root); + var generated = (await new OptionsClassGenerator().GenerateAsync(preserved)).Single().Content; + + await Assert.That(generated).Contains("public void Deconstruct(out string Source)"); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + [Test] public async Task ApiCompatibilityPreserver_Retains_Command_Group_Execute_Facades() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index ef008df7f1a..1e9895f1305 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -94,6 +94,7 @@ private static CliCommandDefinition Preserve( compatibilityProperties, renamedProperties, violations); + RestoreRequiredMemberOrder(baselineProperties, positionalArguments, options); var currentProperties = GetCurrentProperties(positionalArguments, options); foreach (var baseline in baselineProperties) @@ -224,6 +225,12 @@ private static void ValidateMatchingProperty( violations.Add( $"{command.ClassName}.{baseline.PropertyName} changed from required to optional"); } + else if (!baseline.IsRequired && current.IsRequired) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} changed from optional to required " + + "and would remove its public setter"); + } else if (!HasSameCliIdentity(current, baseline)) { violations.Add( @@ -386,6 +393,47 @@ private static void AddRenamedCurrentProperty( ObsoleteMessage = $"Use {forwardToPropertyName} instead.", }); + private static void RestoreRequiredMemberOrder( + IReadOnlyList baselineProperties, + CliPositionalArgument[] positionalArguments, + CliOptionDefinition[] options) + { + var baselineOrder = baselineProperties + .Where(static property => property.IsRequired && !property.IsCompatibility) + .Select((property, index) => (property.PropertyName, index)) + .ToDictionary(pair => pair.PropertyName, pair => pair.index, StringComparer.Ordinal); + RestoreRequiredMemberOrder( + positionalArguments, + static argument => argument.IsRequired, + static argument => argument.PropertyName, + baselineOrder); + RestoreRequiredMemberOrder( + options, + static option => option.IsRequired, + static option => option.PropertyName, + baselineOrder); + } + + private static void RestoreRequiredMemberOrder( + T[] members, + Func isRequired, + Func getPropertyName, + IReadOnlyDictionary baselineOrder) + { + var orderedRequired = members + .Where(isRequired) + .OrderBy(member => baselineOrder.GetValueOrDefault(getPropertyName(member), int.MaxValue)) + .ToArray(); + var requiredIndex = 0; + for (var index = 0; index < members.Length; index++) + { + if (isRequired(members[index])) + { + members[index] = orderedRequired[requiredIndex++]; + } + } + } + private static void AddCompatibilityProperty( ICollection compatibilityProperties, CliCompatibilityProperty property) @@ -423,7 +471,7 @@ private static void PreserveCompatibilityConstructors( var baselineRequired = baselineProperties .Where(static property => property.IsRequired && !property.IsCompatibility) .ToArray(); - if (HasSameConstructorSignature(baselineRequired, currentRequired)) + if (HasSameConstructorContract(baselineRequired, currentRequired)) { return; } @@ -446,6 +494,7 @@ private static void PreserveCompatibilityConstructors( { Parameters = baselineParameters, PrimaryConstructorArguments = primaryArguments, + PreserveDeconstruct = baselineParameters.Length > 0, }, currentRequired); } @@ -455,17 +504,43 @@ private static void AddCompatibilityConstructor( CliCompatibilityConstructor constructor, IReadOnlyList currentRequired) { - if (HasSameConstructorSignature(constructor.Parameters, currentRequired) - || constructors.Any(existing => HasSameConstructorSignature( - existing.Parameters, - constructor.Parameters))) + if (HasSameConstructorSignature(constructor.Parameters, currentRequired)) { return; } + var existing = constructors.FirstOrDefault(candidate => HasSameConstructorSignature( + candidate.Parameters, + constructor.Parameters)); + if (existing is not null) + { + if (constructor.PreserveDeconstruct && !existing.PreserveDeconstruct) + { + constructors.Remove(existing); + constructors.Add(existing with { PreserveDeconstruct = true }); + } + + return; + } + constructors.Add(constructor); } + private static bool HasSameConstructorContract( + IReadOnlyList left, + IReadOnlyList right) + where TLeft : notnull + where TRight : notnull + { + if (left.Count != right.Count) + { + return false; + } + + return left.Select(GetConstructorParameterContract) + .SequenceEqual(right.Select(GetConstructorParameterContract)); + } + private static bool HasSameConstructorSignature( IReadOnlyList left, IReadOnlyList right) @@ -488,6 +563,15 @@ private static bool HasSameConstructorSignature( _ => throw new ArgumentOutOfRangeException(nameof(parameter)), }; + private static (string PropertyName, string CSharpType) GetConstructorParameterContract(T parameter) => + parameter switch + { + GeneratedApiProperty property => (property.PropertyName, property.CSharpType), + CliCompatibilityConstructorParameter compatibilityParameter => + (compatibilityParameter.PropertyName, compatibilityParameter.CSharpType), + _ => throw new ArgumentOutOfRangeException(nameof(parameter)), + }; + private static IReadOnlyDictionary RenameDocumentationExampleValues( IReadOnlyDictionary values, Dictionary renamedProperties) @@ -506,8 +590,8 @@ private static IReadOnlyDictionary RenameDocumentationExampleVal private static GeneratedApiProperty[] GetCurrentProperties( IEnumerable positionalArguments, IEnumerable options) => - positionalArguments.Select(ToGeneratedProperty) - .Concat(options.Select(ToGeneratedProperty)) + options.Select(ToGeneratedProperty) + .Concat(positionalArguments.Select(ToGeneratedProperty)) .ToArray(); private static GeneratedApiProperty ToGeneratedProperty(CliPositionalArgument argument) => @@ -577,18 +661,45 @@ private static CliCompatibilityConstructor[] ReadCompatibilityConstructors( .Where(constructor => constructor.Modifiers.Any(SyntaxKind.PublicKeyword)) .Where(constructor => constructor.Initializer?.IsKind( SyntaxKind.ThisConstructorInitializer) == true) - .Select(constructor => new CliCompatibilityConstructor - { - Parameters = constructor.ParameterList.Parameters - .Select(parameter => new CliCompatibilityConstructorParameter( - parameter.Identifier.ValueText, - parameter.Type?.ToString() ?? string.Empty)) - .ToArray(), - PrimaryConstructorArguments = constructor.Initializer!.ArgumentList.Arguments - .Select(argument => argument.Expression.ToString()) - .ToArray(), - }) + .Select(constructor => ReadCompatibilityConstructor(declaration, constructor)) + .ToArray(); + + private static CliCompatibilityConstructor ReadCompatibilityConstructor( + RecordDeclarationSyntax declaration, + ConstructorDeclarationSyntax constructor) + { + var parameters = constructor.ParameterList.Parameters + .Select(parameter => new CliCompatibilityConstructorParameter( + parameter.Identifier.ValueText, + parameter.Type?.ToString() ?? string.Empty)) .ToArray(); + return new CliCompatibilityConstructor + { + Parameters = parameters, + PrimaryConstructorArguments = constructor.Initializer!.ArgumentList.Arguments + .Select(argument => argument.Expression.ToString()) + .ToArray(), + PreserveDeconstruct = HasMatchingDeconstruct(declaration, parameters), + }; + } + + private static bool HasMatchingDeconstruct( + RecordDeclarationSyntax declaration, + IReadOnlyList parameters) => + declaration.Members + .OfType() + .Where(method => method.Identifier.ValueText.Equals("Deconstruct", StringComparison.Ordinal)) + .Where(method => method.Modifiers.Any(SyntaxKind.PublicKeyword)) + .Any(method => method.ParameterList.Parameters.Count == parameters.Count + && method.ParameterList.Parameters + .Zip(parameters) + .All(pair => pair.First.Modifiers.Any(SyntaxKind.OutKeyword) + && pair.First.Identifier.ValueText.Equals( + pair.Second.PropertyName, + StringComparison.Ordinal) + && (pair.First.Type?.ToString() ?? string.Empty).Equals( + pair.Second.CSharpType, + StringComparison.Ordinal))); private static HashSet ReadExecuteFacadeOptionTypes(string servicesDirectory) { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs index 9bd6e642f5a..90c4c786647 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs @@ -282,6 +282,21 @@ private static void GenerateCompatibilityConstructors( sb.AppendLine(" {"); sb.AppendLine(" }"); sb.AppendLine(); + + if (constructor.PreserveDeconstruct) + { + var deconstructParameters = constructor.Parameters + .Select(parameter => $"out {parameter.CSharpType} {parameter.PropertyName}"); + sb.AppendLine($" public void Deconstruct({string.Join(", ", deconstructParameters)})"); + sb.AppendLine(" {"); + foreach (var parameter in constructor.Parameters) + { + sb.AppendLine($" {parameter.PropertyName} = this.{parameter.PropertyName};"); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + } } } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs index f15fa3fe3a5..63e50135948 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs @@ -210,6 +210,11 @@ public record CliCompatibilityConstructor /// Arguments forwarded to the current primary constructor. /// public required IReadOnlyList PrimaryConstructorArguments { get; init; } + + /// + /// Whether to retain the deconstruction shape synthesized by the former primary constructor. + /// + public bool PreserveDeconstruct { get; init; } } /// From 91a5884fcbcb6046d7d2c171d98786c1824382f3 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:23:55 +0100 Subject: [PATCH 08/21] fix(generator): reject vanished commands --- .../Generators/GeneratorHardeningTests.cs | 77 +++++++++++- .../GeneratedApiCompatibilityPreserver.cs | 112 ++++++++++++++---- 2 files changed, 163 insertions(+), 26 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs index 1a33f533bd5..4ebb1e8ec2b 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs @@ -1228,7 +1228,8 @@ await File.WriteAllTextAsync( "public record ToolEditAddOptions;"); await File.WriteAllTextAsync( Path.Combine(packageDirectory, "Services", "ToolEditAdd.Generated.cs"), - "public class ToolEditAdd { public Task ExecuteAsync(ToolEditAddOptions? options = null) => Task.CompletedTask; }"); + "namespace ModularPipelines.Tool.Services; " + + "public class ToolEditAdd { public Task ExecuteAsync(ToolEditAddOptions? options = null) => Task.CompletedTask; }"); var parent = Command( "ToolEditAddOptions", "ToolOptions", @@ -1255,6 +1256,80 @@ await File.WriteAllTextAsync( } } + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Removed_Command_Facades() + { + var root = Path.Combine(Path.GetTempPath(), $"service-api-{Guid.NewGuid():N}"); + var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool"); + Directory.CreateDirectory(Path.Combine(packageDirectory, "Options")); + Directory.CreateDirectory(Path.Combine(packageDirectory, "Services")); + try + { + await File.WriteAllTextAsync( + Path.Combine(packageDirectory, "Options", "ToolRemovedOptions.Generated.cs"), + "public record ToolRemovedOptions;"); + await File.WriteAllTextAsync( + Path.Combine(packageDirectory, "Services", "Tool.Generated.cs"), + "namespace ModularPipelines.Tool.Services; " + + "public class Tool { public Task RemovedAsync(ToolRemovedOptions? options = null) => Task.CompletedTask; }"); + + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve( + Tool(Command("ToolCurrentOptions", "ToolOptions", ["current"])), + root)); + + await Assert.That(exception.Message) + .Contains("ToolRemovedOptions command disappeared from generated facade"); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + + [Test] + public async Task ApiCompatibilityPreserver_Accepts_Current_Command_Group_Alias_Facades() + { + var root = Path.Combine(Path.GetTempPath(), $"service-api-{Guid.NewGuid():N}"); + var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool"); + Directory.CreateDirectory(Path.Combine(packageDirectory, "Options")); + Directory.CreateDirectory(Path.Combine(packageDirectory, "Services")); + try + { + await File.WriteAllTextAsync( + Path.Combine(packageDirectory, "Options", "ToolBuilderBakeOptions.Generated.cs"), + "public record ToolBuilderBakeOptions;"); + await File.WriteAllTextAsync( + Path.Combine(packageDirectory, "Services", "ToolBuilder.Generated.cs"), + "namespace ModularPipelines.Tool.Services; " + + "public class ToolBuilder { public Task BakeAsync(ToolBuilderBakeOptions? options = null) => Task.CompletedTask; }"); + var tool = Tool(Command( + "ToolBuildxBakeOptions", + "ToolOptions", + ["buildx", "bake"], + subDomainGroup: "Buildx")) with + { + CommandGroupAliases = + [ + new CliCommandGroupAlias + { + Alias = "builder", + CanonicalCommand = "buildx", + ObsoleteMessage = "Use buildx instead.", + }, + ], + }; + + var preserved = GeneratedApiCompatibilityPreserver.Preserve(tool, root); + + await Assert.That(preserved.Commands).HasSingleItem(); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + [Test] public async Task ApiCompatibilityPreserver_Rekeys_Documentation_Examples_After_Required_Rename() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index 1e9895f1305..f471ae35e5a 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -29,8 +29,14 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi var compatibleTool = baseline.TryGetValue($"{tool.NamespacePrefix}Options", out var globalBaseline) ? PreserveGlobalOptions(tool, globalBaseline.Properties) : tool; - var executeFacadeOptionTypes = ReadExecuteFacadeOptionTypes( - Path.Combine(outputDirectory, tool.OutputDirectory, "Services")); + var facadeMethods = ReadFacadeMethods( + Path.Combine(outputDirectory, tool.OutputDirectory, "Services"), + $"{tool.TargetNamespace}.Services"); + RejectRemovedCommands(compatibleTool, facadeMethods); + var executeFacadeOptionTypes = facadeMethods + .Where(static method => method.MethodName.Equals("ExecuteAsync", StringComparison.Ordinal)) + .Select(static method => method.OptionsType) + .ToHashSet(StringComparer.Ordinal); return compatibleTool with { Commands = compatibleTool.Commands @@ -157,6 +163,30 @@ private static void PreserveBaselineProperty( var replacement = currentProperties.FirstOrDefault(property => HasSameCliIdentity(property, baseline)); + if (TryRecordRemovedPropertyViolation(command, baseline, replacement, violations)) + { + return; + } + + AddCompatibilityProperty( + compatibilityProperties, + new CliCompatibilityProperty + { + PropertyName = baseline.PropertyName, + CSharpType = baseline.CSharpType, + ForwardToPropertyName = replacement?.PropertyName, + ObsoleteMessage = replacement is null + ? $"{baseline.PropertyName} is no longer supported by the installed CLI and has no effect." + : $"Use {replacement.PropertyName} instead.", + }); + } + + private static bool TryRecordRemovedPropertyViolation( + CliCommandDefinition command, + GeneratedApiProperty baseline, + GeneratedApiProperty? replacement, + ICollection violations) + { if (replacement is not null && !replacement.CSharpType.Equals(baseline.CSharpType, StringComparison.Ordinal)) { @@ -164,34 +194,24 @@ private static void PreserveBaselineProperty( $"{command.ClassName}.{baseline.PropertyName} changed type from " + $"{baseline.CSharpType} to {replacement.CSharpType} " + $"while being renamed to {replacement.PropertyName}"); - return; + return true; } if (baseline.ArgumentPosition is not null && replacement is null) { violations.Add( $"{command.ClassName}.{baseline.PropertyName} positional argument was removed"); - return; + return true; } if (baseline.IsRequired && replacement is null) { violations.Add( $"{command.ClassName}.{baseline.PropertyName} was removed from the required constructor"); - return; + return true; } - AddCompatibilityProperty( - compatibilityProperties, - new CliCompatibilityProperty - { - PropertyName = baseline.PropertyName, - CSharpType = baseline.CSharpType, - ForwardToPropertyName = replacement?.PropertyName, - ObsoleteMessage = replacement is null - ? $"{baseline.PropertyName} is no longer supported by the installed CLI and has no effect." - : $"Use {replacement.PropertyName} instead.", - }); + return false; } private static void PreserveCompatibilityProperty( @@ -701,12 +721,47 @@ private static bool HasMatchingDeconstruct( pair.Second.CSharpType, StringComparison.Ordinal))); - private static HashSet ReadExecuteFacadeOptionTypes(string servicesDirectory) + private static void RejectRemovedCommands( + CliToolDefinition tool, + IReadOnlyList baselineFacadeMethods) + { + var currentOptionTypes = tool.Commands + .Select(static command => command.ClassName) + .Concat(tool.CommandGroupAliases.SelectMany(alias => tool.Commands + .Where(command => command.CommandParts.Length > 0 + && command.CommandParts[0].Equals( + alias.CanonicalCommand, + StringComparison.OrdinalIgnoreCase)) + .Select(command => GeneratorUtils.GetAliasedClassName(tool, alias, command.ClassName)))) + .ToHashSet(StringComparer.Ordinal); + var removedOptionTypes = baselineFacadeMethods + .Select(static method => method.OptionsType) + .Where(optionsType => !currentOptionTypes.Contains(optionsType)) + .Distinct(StringComparer.Ordinal) + .Order(StringComparer.Ordinal) + .ToArray(); + if (removedOptionTypes.Length == 0) + { + return; + } + + throw new InvalidOperationException( + $"Generated API compatibility validation failed for {tool.ToolName}:" + + Environment.NewLine + + string.Join( + Environment.NewLine, + removedOptionTypes.Select(optionsType => + $"- {optionsType} command disappeared from generated facade"))); + } + + private static IReadOnlyList ReadFacadeMethods( + string servicesDirectory, + string targetNamespace) { - var optionTypes = new HashSet(StringComparer.Ordinal); + var methods = new List(); if (!Directory.Exists(servicesDirectory)) { - return optionTypes; + return methods; } foreach (var path in Directory.EnumerateFiles( @@ -716,20 +771,25 @@ private static HashSet ReadExecuteFacadeOptionTypes(string servicesDirec { var root = CSharpSyntaxTree.ParseText(File.ReadAllText(path)).GetRoot(); foreach (var method in root.DescendantNodes().OfType() - .Where(method => method.Identifier.ValueText.Equals( - "ExecuteAsync", - StringComparison.Ordinal)) + .Where(method => method.Ancestors() + .OfType() + .FirstOrDefault()?.Name.ToString().Equals( + targetNamespace, + StringComparison.Ordinal) == true) .Where(method => method.Modifiers.Any(SyntaxKind.PublicKeyword))) { var optionsType = method.ParameterList.Parameters.FirstOrDefault()?.Type?.ToString(); - if (!string.IsNullOrWhiteSpace(optionsType)) + if (!string.IsNullOrWhiteSpace(optionsType) + && optionsType.TrimEnd('?').EndsWith("Options", StringComparison.Ordinal)) { - optionTypes.Add(optionsType.TrimEnd('?')); + methods.Add(new GeneratedFacadeMethod( + method.Identifier.ValueText, + optionsType.TrimEnd('?'))); } } } - return optionTypes; + return methods; } private static List ReadProperties( @@ -824,3 +884,5 @@ internal sealed record GeneratedApiProperty( internal sealed record GeneratedApiBaseline( IReadOnlyList Properties, IReadOnlyList Constructors); + +internal sealed record GeneratedFacadeMethod(string MethodName, string OptionsType); From e6d80383359cb58d56f640d99f5b4d8b16704fc0 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:38:15 +0100 Subject: [PATCH 09/21] fix(generator): scope facade baselines --- .../Generators/GeneratorHardeningTests.cs | 33 +++++++++++++++++++ .../GeneratedApiCompatibilityPreserver.cs | 8 +++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs index 4ebb1e8ec2b..b92a1cb766a 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs @@ -1287,6 +1287,39 @@ await Assert.That(exception.Message) } } + [Test] + public async Task ApiCompatibilityPreserver_Ignores_Other_Tool_Facades_In_Shared_Package() + { + var root = Path.Combine(Path.GetTempPath(), $"service-api-{Guid.NewGuid():N}"); + var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool"); + Directory.CreateDirectory(Path.Combine(packageDirectory, "Options")); + Directory.CreateDirectory(Path.Combine(packageDirectory, "Services")); + try + { + await File.WriteAllTextAsync( + Path.Combine(packageDirectory, "Options", "ToolCurrentOptions.Generated.cs"), + "public record ToolCurrentOptions;"); + await File.WriteAllTextAsync( + Path.Combine(packageDirectory, "Services", "Tool.Generated.cs"), + "namespace ModularPipelines.Tool.Services; " + + "public class Tool { public Task CurrentAsync(ToolCurrentOptions? options = null) => Task.CompletedTask; }"); + await File.WriteAllTextAsync( + Path.Combine(packageDirectory, "Services", "Other.Generated.cs"), + "namespace ModularPipelines.Tool.Services; " + + "public class Other { public Task RemovedAsync(OtherRemovedOptions? options = null) => Task.CompletedTask; }"); + + var preserved = GeneratedApiCompatibilityPreserver.Preserve( + Tool(Command("ToolCurrentOptions", "ToolOptions", ["current"])), + root); + + await Assert.That(preserved.Commands).HasSingleItem(); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + [Test] public async Task ApiCompatibilityPreserver_Accepts_Current_Command_Group_Alias_Facades() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index f471ae35e5a..3426ec5c7c3 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -31,7 +31,8 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi : tool; var facadeMethods = ReadFacadeMethods( Path.Combine(outputDirectory, tool.OutputDirectory, "Services"), - $"{tool.TargetNamespace}.Services"); + $"{tool.TargetNamespace}.Services", + tool.NamespacePrefix); RejectRemovedCommands(compatibleTool, facadeMethods); var executeFacadeOptionTypes = facadeMethods .Where(static method => method.MethodName.Equals("ExecuteAsync", StringComparison.Ordinal)) @@ -756,7 +757,8 @@ private static void RejectRemovedCommands( private static IReadOnlyList ReadFacadeMethods( string servicesDirectory, - string targetNamespace) + string targetNamespace, + string namespacePrefix) { var methods = new List(); if (!Directory.Exists(servicesDirectory)) @@ -766,7 +768,7 @@ private static IReadOnlyList ReadFacadeMethods( foreach (var path in Directory.EnumerateFiles( servicesDirectory, - "*.Generated.cs", + $"{namespacePrefix}*.Generated.cs", SearchOption.TopDirectoryOnly)) { var root = CSharpSyntaxTree.ParseText(File.ReadAllText(path)).GetRoot(); From ba775ebabb59b3045651c486c85970ad3463b8d5 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:01:07 +0100 Subject: [PATCH 10/21] fix: preserve moved facade methods Track declaring type, method name, and options type so leaf commands retain named facades when they gain children and other method moves fail validation. --- .../Generators/GeneratorHardeningTests.cs | 50 ++++++++++ .../GeneratedApiCompatibilityPreserver.cs | 91 +++++++++++++------ .../Generators/GeneratorUtils.cs | 3 +- .../Generators/SubDomainClassGenerator.cs | 4 +- .../Models/CliCommandDefinition.cs | 5 + 5 files changed, 122 insertions(+), 31 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs index b92a1cb766a..ff88723424b 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs @@ -1287,6 +1287,56 @@ await Assert.That(exception.Message) } } + [Test] + public async Task ApiCompatibilityPreserver_Retains_Leaf_Facade_When_It_Gains_Children() + { + var root = Path.Combine(Path.GetTempPath(), $"service-api-{Guid.NewGuid():N}"); + var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool"); + Directory.CreateDirectory(Path.Combine(packageDirectory, "Options")); + Directory.CreateDirectory(Path.Combine(packageDirectory, "Services")); + try + { + await File.WriteAllTextAsync( + Path.Combine(packageDirectory, "Options", "ToolGroupChildOptions.Generated.cs"), + "public record ToolGroupChildOptions;"); + await File.WriteAllTextAsync( + Path.Combine(packageDirectory, "Services", "ToolGroup.Generated.cs"), + "namespace ModularPipelines.Tool.Services; " + + "public class ToolGroup { public Task ChildAsync(ToolGroupChildOptions? options = null) => Task.CompletedTask; }"); + var child = Command( + "ToolGroupChildOptions", + "ToolOptions", + ["group", "child"], + subDomainGroup: "group"); + var grandchild = Command( + "ToolGroupChildSubOptions", + "ToolOptions", + ["group", "child", "sub"], + subDomainGroup: "group"); + + var preserved = GeneratedApiCompatibilityPreserver.Preserve( + Tool(child, grandchild), + root); + var generated = await new SubDomainClassGenerator().GenerateAsync(preserved); + var groupFacade = generated.Single(file => Path.GetFileName(file.RelativePath) + .Equals("ToolGroup.Generated.cs", StringComparison.Ordinal)); + var childFacade = generated.Single(file => Path.GetFileName(file.RelativePath) + .Equals("ToolGroupChild.Generated.cs", StringComparison.Ordinal)); + + using (Assert.Multiple()) + { + await Assert.That(groupFacade.Content) + .Contains("Task ChildAsync("); + await Assert.That(childFacade.Content) + .Contains("Task ExecuteAsync("); + } + } + finally + { + Directory.Delete(root, recursive: true); + } + } + [Test] public async Task ApiCompatibilityPreserver_Ignores_Other_Tool_Facades_In_Shared_Package() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index 3426ec5c7c3..fb146b9cfd0 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -33,12 +33,15 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi Path.Combine(outputDirectory, tool.OutputDirectory, "Services"), $"{tool.TargetNamespace}.Services", tool.NamespacePrefix); - RejectRemovedCommands(compatibleTool, facadeMethods); var executeFacadeOptionTypes = facadeMethods .Where(static method => method.MethodName.Equals("ExecuteAsync", StringComparison.Ordinal)) .Select(static method => method.OptionsType) .ToHashSet(StringComparer.Ordinal); - return compatibleTool with + var namedFacadeOptionTypes = facadeMethods + .Where(static method => !method.MethodName.Equals("ExecuteAsync", StringComparison.Ordinal)) + .Select(static method => method.OptionsType) + .ToHashSet(StringComparer.Ordinal); + var preservedTool = compatibleTool with { Commands = compatibleTool.Commands .Select(command => baseline.TryGetValue(command.ClassName, out var commandBaseline) @@ -47,8 +50,13 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi .Select(command => executeFacadeOptionTypes.Contains(command.ClassName) ? command with { PreserveExecuteFacade = true } : command) + .Select(command => namedFacadeOptionTypes.Contains(command.ClassName) + ? command with { PreserveNamedFacade = true } + : command) .ToArray(), }; + RejectRemovedFacadeMethods(preservedTool, facadeMethods); + return preservedTool; } internal static CliToolDefinition PreserveGlobalOptions( @@ -722,26 +730,19 @@ private static bool HasMatchingDeconstruct( pair.Second.CSharpType, StringComparison.Ordinal))); - private static void RejectRemovedCommands( + private static void RejectRemovedFacadeMethods( CliToolDefinition tool, IReadOnlyList baselineFacadeMethods) { - var currentOptionTypes = tool.Commands - .Select(static command => command.ClassName) - .Concat(tool.CommandGroupAliases.SelectMany(alias => tool.Commands - .Where(command => command.CommandParts.Length > 0 - && command.CommandParts[0].Equals( - alias.CanonicalCommand, - StringComparison.OrdinalIgnoreCase)) - .Select(command => GeneratorUtils.GetAliasedClassName(tool, alias, command.ClassName)))) - .ToHashSet(StringComparer.Ordinal); - var removedOptionTypes = baselineFacadeMethods - .Select(static method => method.OptionsType) - .Where(optionsType => !currentOptionTypes.Contains(optionsType)) - .Distinct(StringComparer.Ordinal) - .Order(StringComparer.Ordinal) + var currentFacadeMethods = GenerateFacadeMethods(tool).ToHashSet(); + var removedMethods = baselineFacadeMethods + .Where(method => !currentFacadeMethods.Contains(method)) + .Distinct() + .OrderBy(static method => method.DeclaringType, StringComparer.Ordinal) + .ThenBy(static method => method.MethodName, StringComparer.Ordinal) + .ThenBy(static method => method.OptionsType, StringComparer.Ordinal) .ToArray(); - if (removedOptionTypes.Length == 0) + if (removedMethods.Length == 0) { return; } @@ -751,8 +752,23 @@ private static void RejectRemovedCommands( + Environment.NewLine + string.Join( Environment.NewLine, - removedOptionTypes.Select(optionsType => - $"- {optionsType} command disappeared from generated facade"))); + removedMethods.Select(method => + $"- {method.DeclaringType}.{method.MethodName}({method.OptionsType}): " + + $"{method.OptionsType} command disappeared from generated facade"))); + } + + private static IReadOnlyList GenerateFacadeMethods(CliToolDefinition tool) + { + var generatedFiles = new List(); + generatedFiles.AddRange( + new ServiceInterfaceGenerator().GenerateAsync(tool).GetAwaiter().GetResult()); + generatedFiles.AddRange( + new ServiceImplementationGenerator().GenerateAsync(tool).GetAwaiter().GetResult()); + generatedFiles.AddRange( + new SubDomainClassGenerator().GenerateAsync(tool).GetAwaiter().GetResult()); + return ReadFacadeMethods( + generatedFiles.Select(static file => file.Content), + $"{tool.TargetNamespace}.Services"); } private static IReadOnlyList ReadFacadeMethods( @@ -760,18 +776,27 @@ private static IReadOnlyList ReadFacadeMethods( string targetNamespace, string namespacePrefix) { - var methods = new List(); if (!Directory.Exists(servicesDirectory)) { - return methods; + return []; } - foreach (var path in Directory.EnumerateFiles( - servicesDirectory, - $"{namespacePrefix}*.Generated.cs", - SearchOption.TopDirectoryOnly)) + var sources = Directory.EnumerateFiles( + servicesDirectory, + $"{namespacePrefix}*.Generated.cs", + SearchOption.TopDirectoryOnly) + .Select(File.ReadAllText); + return ReadFacadeMethods(sources, targetNamespace); + } + + private static IReadOnlyList ReadFacadeMethods( + IEnumerable sources, + string targetNamespace) + { + var methods = new List(); + foreach (var source in sources) { - var root = CSharpSyntaxTree.ParseText(File.ReadAllText(path)).GetRoot(); + var root = CSharpSyntaxTree.ParseText(source).GetRoot(); foreach (var method in root.DescendantNodes().OfType() .Where(method => method.Ancestors() .OfType() @@ -781,10 +806,15 @@ private static IReadOnlyList ReadFacadeMethods( .Where(method => method.Modifiers.Any(SyntaxKind.PublicKeyword))) { var optionsType = method.ParameterList.Parameters.FirstOrDefault()?.Type?.ToString(); - if (!string.IsNullOrWhiteSpace(optionsType) + var declaringType = method.Ancestors() + .OfType() + .FirstOrDefault()?.Identifier.ValueText; + if (!string.IsNullOrWhiteSpace(declaringType) + && !string.IsNullOrWhiteSpace(optionsType) && optionsType.TrimEnd('?').EndsWith("Options", StringComparison.Ordinal)) { methods.Add(new GeneratedFacadeMethod( + declaringType, method.Identifier.ValueText, optionsType.TrimEnd('?'))); } @@ -887,4 +917,7 @@ internal sealed record GeneratedApiBaseline( IReadOnlyList Properties, IReadOnlyList Constructors); -internal sealed record GeneratedFacadeMethod(string MethodName, string OptionsType); +internal sealed record GeneratedFacadeMethod( + string DeclaringType, + string MethodName, + string OptionsType); diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs index 210e1dd9c52..001bd24ad3b 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs @@ -988,7 +988,8 @@ public static IReadOnlyList GetNonCollidingRootCommands(Cl var rootCommands = tool.Commands .Where(c => c.SubDomainGroup is null) - .Where(c => !subDomainNames.Contains(GetCommandGroupIdentifier(c))) + .Where(c => c.PreserveNamedFacade + || !subDomainNames.Contains(GetCommandGroupIdentifier(c))) .ToList(); // Distinct commands can normalize to the same method name (e.g. "build-server" diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs index 7d7467264c0..95d1d419821 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs @@ -100,7 +100,9 @@ private static void GenerateFilesFromTree( } } - var excludedCommands = collidingCommands.Values.ToHashSet(); + var excludedCommands = collidingCommands.Values + .Where(static command => !command.PreserveNamedFacade) + .ToHashSet(); // Generate the command represented by this node as ExecuteAsync(). Root nodes receive // their top-level command; nested nodes receive a command that collided with the diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs index 63e50135948..66705ed172e 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs @@ -102,6 +102,11 @@ public record CliCommandDefinition /// public bool PreserveExecuteFacade { get; init; } + /// + /// Whether an existing named command facade must remain when the command gains children. + /// + public bool PreserveNamedFacade { get; init; } + /// /// Public methods retained as obsolete forwarding aliases for compatibility. /// From b10f266a2610c67186c663029aea49a86e3574b1 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:17:00 +0100 Subject: [PATCH 11/21] fix(options): keep key paths visible --- .../Options/FluxBootstrapBitbucketServerOptions.Generated.cs | 1 - .../Options/FluxBootstrapGitOptions.Generated.cs | 1 - .../Options/FluxBootstrapGiteaOptions.Generated.cs | 1 - .../Options/FluxBootstrapGithubOptions.Generated.cs | 1 - .../Options/FluxBootstrapGitlabOptions.Generated.cs | 1 - .../Options/FluxBootstrapOptions.Generated.cs | 1 - .../Options/FluxCreateSecretGitOptions.Generated.cs | 1 - .../Options/FluxCreateSourceGitOptions.Generated.cs | 1 - .../Options/PulumiDeploymentRunOptions.Generated.cs | 1 - 9 files changed, 9 deletions(-) diff --git a/src/ModularPipelines.Flux/Options/FluxBootstrapBitbucketServerOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBootstrapBitbucketServerOptions.Generated.cs index 748fcfdb41a..cd468a7026d 100644 --- a/src/ModularPipelines.Flux/Options/FluxBootstrapBitbucketServerOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxBootstrapBitbucketServerOptions.Generated.cs @@ -290,7 +290,6 @@ public record FluxBootstrapBitbucketServerOptions : FluxOptions /// /// path to a private key file used for authenticating to the Git SSH server /// - [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxBootstrapGitOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBootstrapGitOptions.Generated.cs index d0ea2f8d0e1..6ef0774c391 100644 --- a/src/ModularPipelines.Flux/Options/FluxBootstrapGitOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxBootstrapGitOptions.Generated.cs @@ -273,7 +273,6 @@ public record FluxBootstrapGitOptions : FluxOptions /// /// path to a private key file used for authenticating to the Git SSH server /// - [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxBootstrapGiteaOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBootstrapGiteaOptions.Generated.cs index ec7a21189d2..e260dc6c3c6 100644 --- a/src/ModularPipelines.Flux/Options/FluxBootstrapGiteaOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxBootstrapGiteaOptions.Generated.cs @@ -284,7 +284,6 @@ public record FluxBootstrapGiteaOptions : FluxOptions /// /// path to a private key file used for authenticating to the Git SSH server /// - [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxBootstrapGithubOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBootstrapGithubOptions.Generated.cs index a073c0541d5..ba2d567f5e8 100644 --- a/src/ModularPipelines.Flux/Options/FluxBootstrapGithubOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxBootstrapGithubOptions.Generated.cs @@ -284,7 +284,6 @@ public record FluxBootstrapGithubOptions : FluxOptions /// /// path to a private key file used for authenticating to the Git SSH server /// - [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxBootstrapGitlabOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBootstrapGitlabOptions.Generated.cs index db539dd827d..01857ddffc2 100644 --- a/src/ModularPipelines.Flux/Options/FluxBootstrapGitlabOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxBootstrapGitlabOptions.Generated.cs @@ -290,7 +290,6 @@ public record FluxBootstrapGitlabOptions : FluxOptions /// /// path to a private key file used for authenticating to the Git SSH server /// - [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxBootstrapOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxBootstrapOptions.Generated.cs index 3e355237676..0f276e3f3d0 100644 --- a/src/ModularPipelines.Flux/Options/FluxBootstrapOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxBootstrapOptions.Generated.cs @@ -122,7 +122,6 @@ public record FluxBootstrapOptions : FluxOptions /// /// path to a private key file used for authenticating to the Git SSH server /// - [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxCreateSecretGitOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxCreateSecretGitOptions.Generated.cs index b2c134da88f..ccb7a79f150 100644 --- a/src/ModularPipelines.Flux/Options/FluxCreateSecretGitOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxCreateSecretGitOptions.Generated.cs @@ -50,7 +50,6 @@ public record FluxCreateSecretGitOptions : FluxOptions /// /// path to a passwordless private key file used for authenticating to the Git SSH server /// - [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Flux/Options/FluxCreateSourceGitOptions.Generated.cs b/src/ModularPipelines.Flux/Options/FluxCreateSourceGitOptions.Generated.cs index 8c1d4168ba7..7f016bbca86 100644 --- a/src/ModularPipelines.Flux/Options/FluxCreateSourceGitOptions.Generated.cs +++ b/src/ModularPipelines.Flux/Options/FluxCreateSourceGitOptions.Generated.cs @@ -61,7 +61,6 @@ public record FluxCreateSourceGitOptions : FluxOptions /// /// path to a passwordless private key file used for authenticating to the Git SSH server /// - [SecretValue] [CliOption("--private-key-file", Format = OptionFormat.EqualsSeparated)] public string? PrivateKeyFile { get; set; } diff --git a/src/ModularPipelines.Pulumi/Options/PulumiDeploymentRunOptions.Generated.cs b/src/ModularPipelines.Pulumi/Options/PulumiDeploymentRunOptions.Generated.cs index 57cdb774312..e34cc0ca75e 100644 --- a/src/ModularPipelines.Pulumi/Options/PulumiDeploymentRunOptions.Generated.cs +++ b/src/ModularPipelines.Pulumi/Options/PulumiDeploymentRunOptions.Generated.cs @@ -71,7 +71,6 @@ public record PulumiDeploymentRunOptions( /// /// Git SSH private key path; use --git-auth-password for the password, if needed /// - [SecretValue] [CliOption("--git-auth-ssh-private-key-path", Format = OptionFormat.EqualsSeparated)] public string? GitAuthSshPrivateKeyPath { get; set; } From 226d248796ed92a756e12b8e64969d78eb745ef5 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:45:30 +0100 Subject: [PATCH 12/21] fix: keep secret config paths visible --- .../TrivyFilesystemOptions.Generated.cs | 1 - .../Options/TrivyImageOptions.Generated.cs | 1 - .../TrivyKubernetesOptions.Generated.cs | 1 - .../TrivyRepositoryOptions.Generated.cs | 1 - .../Options/TrivyRootfsOptions.Generated.cs | 1 - .../Options/TrivyVmOptions.Generated.cs | 1 - .../Scrapers/CosignCliScraperTests.cs | 2 +- .../Scrapers/TrivyCliScraperTests.cs | 54 ++++++++++++++++++- .../Scrapers/Cli/CobraCliScraper.cs | 9 ++-- .../Scrapers/Cli/CosignCliScraper.cs | 7 ++- .../Scrapers/Cli/TrivyCliScraper.cs | 25 ++++++++- 11 files changed, 88 insertions(+), 15 deletions(-) diff --git a/src/ModularPipelines.Trivy/Options/TrivyFilesystemOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyFilesystemOptions.Generated.cs index 2ad4b612f92..1f0afe3ed23 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyFilesystemOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyFilesystemOptions.Generated.cs @@ -510,7 +510,6 @@ public record TrivyFilesystemOptions( /// /// specify a path to config file for secret scanning (default "trivy-secret.yaml") /// - [SecretValue] [CliOption("--secret-config", Format = OptionFormat.EqualsSeparated)] public string? SecretConfig { get; set; } diff --git a/src/ModularPipelines.Trivy/Options/TrivyImageOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyImageOptions.Generated.cs index e71a8d65298..125a6cb9bd4 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyImageOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyImageOptions.Generated.cs @@ -544,7 +544,6 @@ public record TrivyImageOptions : TrivyOptions /// /// specify a path to config file for secret scanning (default "trivy-secret.yaml") /// - [SecretValue] [CliOption("--secret-config", Format = OptionFormat.EqualsSeparated)] public string? SecretConfig { get; set; } diff --git a/src/ModularPipelines.Trivy/Options/TrivyKubernetesOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyKubernetesOptions.Generated.cs index 02f2544ecc9..b6e22a7caee 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyKubernetesOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyKubernetesOptions.Generated.cs @@ -498,7 +498,6 @@ public record TrivyKubernetesOptions : TrivyOptions /// /// specify a path to config file for secret scanning (default "trivy-secret.yaml") /// - [SecretValue] [CliOption("--secret-config", Format = OptionFormat.EqualsSeparated)] public string? SecretConfig { get; set; } diff --git a/src/ModularPipelines.Trivy/Options/TrivyRepositoryOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyRepositoryOptions.Generated.cs index 126b66fe132..49e13f7c3ed 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyRepositoryOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyRepositoryOptions.Generated.cs @@ -492,7 +492,6 @@ public record TrivyRepositoryOptions( /// /// specify a path to config file for secret scanning (default "trivy-secret.yaml") /// - [SecretValue] [CliOption("--secret-config", Format = OptionFormat.EqualsSeparated)] public string? SecretConfig { get; set; } diff --git a/src/ModularPipelines.Trivy/Options/TrivyRootfsOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyRootfsOptions.Generated.cs index 012faa8834c..66219f15d3d 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyRootfsOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyRootfsOptions.Generated.cs @@ -498,7 +498,6 @@ public record TrivyRootfsOptions( /// /// specify a path to config file for secret scanning (default "trivy-secret.yaml") /// - [SecretValue] [CliOption("--secret-config", Format = OptionFormat.EqualsSeparated)] public string? SecretConfig { get; set; } diff --git a/src/ModularPipelines.Trivy/Options/TrivyVmOptions.Generated.cs b/src/ModularPipelines.Trivy/Options/TrivyVmOptions.Generated.cs index 28986eba79f..4b48eb3110e 100644 --- a/src/ModularPipelines.Trivy/Options/TrivyVmOptions.Generated.cs +++ b/src/ModularPipelines.Trivy/Options/TrivyVmOptions.Generated.cs @@ -406,7 +406,6 @@ public record TrivyVmOptions( /// /// specify a path to config file for secret scanning (default "trivy-secret.yaml") /// - [SecretValue] [CliOption("--secret-config", Format = OptionFormat.EqualsSeparated)] public string? SecretConfig { get; set; } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/CosignCliScraperTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/CosignCliScraperTests.cs index 03b964c17a3..b5a3b986449 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/CosignCliScraperTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/CosignCliScraperTests.cs @@ -145,6 +145,6 @@ public IReadOnlyList ApplyFix( IReadOnlyList positionalArguments) => ApplyPositionalArgumentFixes(commandParts, positionalArguments); - public bool IsSecret(string propertyName, bool isFlag) => IsSecretOption(propertyName, isFlag); + public bool IsSecret(string propertyName, bool isFlag) => IsSecretOption(propertyName, isFlag, string.Empty); } } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/TrivyCliScraperTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/TrivyCliScraperTests.cs index 9221cc6eae4..0e2699c28cf 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/TrivyCliScraperTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/TrivyCliScraperTests.cs @@ -80,6 +80,18 @@ await Assert.That(scraper.NormalizeDescription(description)) } } + [Test] + public async Task Home_Directory_Path_Is_Normalized_Across_Platforms() + { + var scraper = new TestTrivyCliScraper(); + + var description = scraper.NormalizeDescription( + @"module directory (default ""C:\Users\runneradmin\.trivy\modules"")"); + + await Assert.That(description) + .IsEqualTo("module directory (default \"/.trivy/modules\")"); + } + [Test] public async Task Image_Help_Parses_Target_Types_And_Secrets() { @@ -117,6 +129,25 @@ await Assert.That(command.Options.Single(x => x.SwitchName == "--timeout").CShar await Assert.That(command.Options.Single(x => x.SwitchName == "--password").IsSecret).IsTrue(); } + [Test] + public async Task Secret_Config_Path_Is_Not_Classified_As_Secret() + { + const string helpText = """ + Scan a container image + + Usage: + trivy image [flags] IMAGE_NAME + + Secret Flags + --secret-config string specify a path to config file for secret scanning + """; + + var command = await new TestTrivyCliScraper().Parse(["trivy", "image"], helpText); + var secretConfig = command!.Options.Single(x => x.SwitchName == "--secret-config"); + + await Assert.That(secretConfig.IsSecret).IsFalse(); + } + [Test] public async Task Plugin_Upgrade_Accepts_Multiple_Optional_Names() { @@ -160,7 +191,7 @@ trivy plugin run [flags] NAME | URL | FILE_PATH await Assert.That(positionals[1].PropertyName).IsEqualTo("PluginArguments"); await Assert.That(positionals[1].CSharpType).IsEqualTo("IEnumerable?"); await Assert.That(positionals[1].IsRequired).IsFalse(); - await Assert.That(positionals[1].PositionIndex).IsEqualTo(1); + await Assert.That(positionals[1].PositionIndex).IsEqualTo(0); await Assert.That(positionals[1].Phase).IsEqualTo(CommandLinePhase.Passthrough); } @@ -185,6 +216,27 @@ trivy vex repo download [REPO_NAMES] [flags] await Assert.That(positional.IsRequired).IsFalse(); } + [Test] + public async Task Vex_Command_Preserves_Optional_Command_Operand() + { + const string helpText = """ + [EXPERIMENTAL] VEX utilities + + Usage: + trivy vex [command] + + Available Commands: + repo Manage VEX repositories + """; + + var command = await new TestTrivyCliScraper().Parse(["trivy", "vex"], helpText); + var positional = command!.PositionalArguments.Single(); + + await Assert.That(positional.PropertyName).IsEqualTo("Command"); + await Assert.That(positional.CSharpType).IsEqualTo("string?"); + await Assert.That(positional.IsRequired).IsFalse(); + } + private sealed class TestTrivyCliScraper : TrivyCliScraper { public TestTrivyCliScraper() diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs index 1c1098e7589..96f2802fc36 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs @@ -386,7 +386,7 @@ private List ParseOptions(string helpText, string[] command ? CliOptionValueArity.Optional : CliOptionValueArity.Required, EnumDefinition = enumDef, - IsSecret = !isBoolean && IsSecretOption(propertyName, isFlag) + IsSecret = !isBoolean && IsSecretOption(propertyName, isFlag, description) }); } } @@ -440,8 +440,11 @@ private static string NormalizeTypeHint(string typeHint, bool hasDefaultValue) /// /// Determines whether a tool option value must be masked in command logs. /// - protected virtual bool IsSecretOption(string propertyName, bool isFlag) => - GeneratorUtils.IsSecretOption(propertyName, isFlag); + protected virtual bool IsSecretOption( + string propertyName, + bool isFlag, + string description) => + GeneratorUtils.IsSecretOption(propertyName, isFlag, description); /// /// Determines whether an option's documented values form a closed set. diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CosignCliScraper.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CosignCliScraper.cs index 5f0be1f967b..f3aaaf8116c 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CosignCliScraper.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CosignCliScraper.cs @@ -85,8 +85,11 @@ protected override IReadOnlyList ApplyPositionalArgumentF }; } - protected override bool IsSecretOption(string propertyName, bool isFlag) => - base.IsSecretOption(propertyName, isFlag) || + protected override bool IsSecretOption( + string propertyName, + bool isFlag, + string description) => + base.IsSecretOption(propertyName, isFlag, description) || (!isFlag && (propertyName.Equals("NewKey", StringComparison.OrdinalIgnoreCase) || propertyName.Equals("OldKey", StringComparison.OrdinalIgnoreCase) || diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/TrivyCliScraper.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/TrivyCliScraper.cs index 97916f0c083..a0527964399 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/TrivyCliScraper.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/TrivyCliScraper.cs @@ -122,7 +122,7 @@ private static IReadOnlyList CreatePluginRunArguments( { CSharpType = "IEnumerable?", Phase = CommandLinePhase.Passthrough, - PositionIndex = 1, + PositionIndex = 0, }, ]; } @@ -148,15 +148,33 @@ private static IReadOnlyList CreateMissingPositionalArgum [RequiredArgument("Source")], ["registry", "login"] or ["registry", "logout"] => [RequiredArgument("Server")], + ["module"] or ["plugin"] or ["registry"] or ["vex"] or ["vex", "repo"] => + [OptionalArgument("Command") with { Description = "The command operand." }], _ => positionalArguments, }; protected override string NormalizeOptionDescription(string description) { - var normalizedDescription = UserHomeDirectoryPattern().Replace(description, ""); + var normalizedDescription = NormalizeHomePathSeparators( + UserHomeDirectoryPattern().Replace(description, "")); return TrivyCacheDirectoryPattern().Replace(normalizedDescription, "/trivy"); } + private static string NormalizeHomePathSeparators(string description) + { + const string homePlaceholder = ""; + var homeEnd = description.IndexOf(homePlaceholder, StringComparison.Ordinal); + if (homeEnd < 0) + { + return description; + } + + homeEnd += homePlaceholder.Length; + return string.Concat( + description.AsSpan(0, homeEnd), + PathSeparatorPattern().Replace(description[homeEnd..], "/")); + } + private static CliPositionalArgument RequiredArgument(string propertyName) => new() { PropertyName = propertyName, @@ -177,4 +195,7 @@ private static CliPositionalArgument OptionalArgument(string propertyName) => [GeneratedRegex(@"(?i)(?:[\\/]+AppData[\\/]+Local|[\\/]+\.cache|[\\/]+Library[\\/]+Caches)[\\/]+trivy")] private static partial Regex TrivyCacheDirectoryPattern(); + + [GeneratedRegex(@"[\\/]+")] + private static partial Regex PathSeparatorPattern(); } From 13f8283aaa5522fa3ee2762e0b502ea55f07a738 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:56:20 +0100 Subject: [PATCH 13/21] fix(generator): preserve optional facades --- .../Generators/GeneratorHardeningTests.cs | 48 +++++++++++++++++++ .../GeneratedApiCompatibilityPreserver.cs | 16 +++++-- .../Generators/GeneratorUtils.cs | 16 +++++-- .../Generators/SubDomainClassGenerator.cs | 2 +- .../Models/CliCommandDefinition.cs | 5 ++ 5 files changed, 80 insertions(+), 7 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs index ff88723424b..194d008800e 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs @@ -1287,6 +1287,54 @@ await Assert.That(exception.Message) } } + [Test] + public async Task ApiCompatibilityPreserver_Retains_Optional_Facade_When_Required_Member_Is_Added() + { + var root = Path.Combine(Path.GetTempPath(), $"service-api-{Guid.NewGuid():N}"); + var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool"); + Directory.CreateDirectory(Path.Combine(packageDirectory, "Options")); + Directory.CreateDirectory(Path.Combine(packageDirectory, "Services")); + try + { + await File.WriteAllTextAsync( + Path.Combine(packageDirectory, "Options", "ToolAddOptions.Generated.cs"), + "public record ToolAddOptions;"); + await File.WriteAllTextAsync( + Path.Combine(packageDirectory, "Services", "Tool.Generated.cs"), + "namespace ModularPipelines.Tool.Services; " + + "public class Tool { public Task AddAsync(ToolAddOptions? options = null) => Task.CompletedTask; }"); + var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with + { + PositionalArguments = + [ + new CliPositionalArgument + { + PropertyName = "Package", + CSharpType = "string", + IsRequired = true, + PositionIndex = 0, + }, + ], + }; + + var preserved = GeneratedApiCompatibilityPreserver.Preserve(Tool(command), root); + var generatedOptions = (await new OptionsClassGenerator().GenerateAsync(preserved)).Single().Content; + var generatedInterface = (await new ServiceInterfaceGenerator().GenerateAsync(preserved)).Single().Content; + var generatedImplementation = (await new ServiceImplementationGenerator().GenerateAsync(preserved)).Single().Content; + + using (Assert.Multiple()) + { + await Assert.That(generatedOptions).Contains("public ToolAddOptions()"); + await Assert.That(generatedInterface).Contains("AddAsync(ToolAddOptions? options = null"); + await Assert.That(generatedImplementation).Contains("options ?? new ToolAddOptions()"); + } + } + finally + { + Directory.Delete(root, recursive: true); + } + } + [Test] public async Task ApiCompatibilityPreserver_Retains_Leaf_Facade_When_It_Gains_Children() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index fb146b9cfd0..181434d9032 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -41,6 +41,10 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi .Where(static method => !method.MethodName.Equals("ExecuteAsync", StringComparison.Ordinal)) .Select(static method => method.OptionsType) .ToHashSet(StringComparer.Ordinal); + var optionalFacadeOptionTypes = facadeMethods + .Where(static method => method.IsOptionsOptional) + .Select(static method => method.OptionsType) + .ToHashSet(StringComparer.Ordinal); var preservedTool = compatibleTool with { Commands = compatibleTool.Commands @@ -53,6 +57,9 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi .Select(command => namedFacadeOptionTypes.Contains(command.ClassName) ? command with { PreserveNamedFacade = true } : command) + .Select(command => optionalFacadeOptionTypes.Contains(command.ClassName) + ? command with { PreserveOptionalOptionsParameter = true } + : command) .ToArray(), }; RejectRemovedFacadeMethods(preservedTool, facadeMethods); @@ -805,7 +812,8 @@ private static IReadOnlyList ReadFacadeMethods( StringComparison.Ordinal) == true) .Where(method => method.Modifiers.Any(SyntaxKind.PublicKeyword))) { - var optionsType = method.ParameterList.Parameters.FirstOrDefault()?.Type?.ToString(); + var optionsParameter = method.ParameterList.Parameters.FirstOrDefault(); + var optionsType = optionsParameter?.Type?.ToString(); var declaringType = method.Ancestors() .OfType() .FirstOrDefault()?.Identifier.ValueText; @@ -816,7 +824,8 @@ private static IReadOnlyList ReadFacadeMethods( methods.Add(new GeneratedFacadeMethod( declaringType, method.Identifier.ValueText, - optionsType.TrimEnd('?'))); + optionsType.TrimEnd('?'), + optionsParameter?.Default is not null)); } } } @@ -920,4 +929,5 @@ internal sealed record GeneratedApiBaseline( internal sealed record GeneratedFacadeMethod( string DeclaringType, string MethodName, - string OptionsType); + string OptionsType, + bool IsOptionsOptional); diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs index 001bd24ad3b..67b288cba21 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs @@ -629,11 +629,21 @@ internal static IReadOnlyList GetRequiredConstruct /// public static string BuildOptionsParameter(CliCommandDefinition command) { - return HasRequiredParameters(command) + return RequiresOptionsParameter(command) ? $"{command.ClassName} options" : $"{command.ClassName}? options = null"; } + /// + /// Whether callers must supply the generated options object. + /// + public static bool RequiresOptionsParameter(CliCommandDefinition command) + { + ArgumentNullException.ThrowIfNull(command); + + return HasRequiredParameters(command) && !command.PreserveOptionalOptionsParameter; + } + /// /// Generates a command method signature for a service interface. /// @@ -697,7 +707,7 @@ public static void GenerateServiceMethod( ArgumentNullException.ThrowIfNull(command); methodName = EnsureAsyncSuffix(methodName); - var hasRequiredParams = HasRequiredParameters(command); + var requiresOptions = RequiresOptionsParameter(command); // XML documentation if (includeXmlDoc && !string.IsNullOrEmpty(command.Description)) @@ -719,7 +729,7 @@ public static void GenerateServiceMethod( sb.AppendLine($"{indent} CancellationToken cancellationToken = default)"); sb.AppendLine($"{indent}{{"); - if (hasRequiredParams) + if (requiresOptions) { sb.AppendLine($"{indent} return await _command.ExecuteCommandLineToolAsync(options, executionOptions, cancellationToken);"); } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs index 95d1d419821..c3035f097b3 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs @@ -415,7 +415,7 @@ private static string BuildCompatibilityOptionsParameter( tool, alias, command.ClassName); - return GeneratorUtils.HasRequiredParameters(command) + return GeneratorUtils.RequiresOptionsParameter(command) ? $"{aliasOptionsClassName} options" : $"{aliasOptionsClassName}? options = null"; } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs index 66705ed172e..e7536800c39 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs @@ -107,6 +107,11 @@ public record CliCommandDefinition /// public bool PreserveNamedFacade { get; init; } + /// + /// Whether an existing facade allowed its options argument to be omitted. + /// + public bool PreserveOptionalOptionsParameter { get; init; } + /// /// Public methods retained as obsolete forwarding aliases for compatibility. /// From b73f339bb6a51d54817658c1e304136d16a9e468 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:13:57 +0100 Subject: [PATCH 14/21] fix(generator): preserve alias constructors --- .../Generators/GeneratorHardeningTests.cs | 107 ++++++++++++++++++ .../GeneratedApiCompatibilityPreserver.cs | 98 +++++++++++++++- .../Generators/GeneratorUtils.cs | 16 +++ .../Generators/OptionsClassGenerator.cs | 40 +++---- .../Models/CliCommandDefinition.cs | 6 + 5 files changed, 241 insertions(+), 26 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs index 194d008800e..7fe31d197c6 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs @@ -1461,6 +1461,113 @@ await File.WriteAllTextAsync( } } + [Test] + public async Task ApiCompatibilityPreserver_Retains_Command_Group_Alias_Constructors() + { + var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}"); + var optionsDirectory = Path.Combine(root, "src", "ModularPipelines.Tool", "Options"); + Directory.CreateDirectory(optionsDirectory); + try + { + await File.WriteAllTextAsync( + Path.Combine(optionsDirectory, "ToolBuildxBakeOptions.Generated.cs"), + "public record ToolBuildxBakeOptions([property: CliOption(\"--source\")] string Source);"); + await File.WriteAllTextAsync( + Path.Combine(optionsDirectory, "ToolBuilderBakeOptions.Generated.cs"), + "public record ToolBuilderBakeOptions : ToolBuildxBakeOptions " + + "{ public ToolBuilderBakeOptions(string Source) : base(Source) { } }"); + var command = Command( + "ToolBuildxBakeOptions", + "ToolOptions", + ["buildx", "bake"], + subDomainGroup: "Buildx") with + { + Options = + [ + RequiredOption("--source", "Source"), + RequiredOption("--destination", "Destination"), + ], + }; + var tool = Tool(command) with + { + CommandGroupAliases = + [ + new CliCommandGroupAlias + { + Alias = "builder", + CanonicalCommand = "buildx", + ObsoleteMessage = "Use buildx instead.", + }, + ], + }; + + var preserved = GeneratedApiCompatibilityPreserver.Preserve(tool, root); + var generatedAlias = (await new OptionsClassGenerator().GenerateAsync(preserved)) + .Single(file => Path.GetFileName(file.RelativePath) + .Equals("ToolBuilderBakeOptions.Generated.cs", StringComparison.Ordinal)) + .Content; + + await Assert.That(generatedAlias) + .Contains("public ToolBuilderBakeOptions(string Source)"); + await Assert.That(generatedAlias) + .Contains(": this(Source, default!)"); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + + [Test] + public async Task ApiCompatibilityPreserver_Retains_Parameterless_Command_Group_Alias_Constructor() + { + var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}"); + var optionsDirectory = Path.Combine(root, "src", "ModularPipelines.Tool", "Options"); + Directory.CreateDirectory(optionsDirectory); + try + { + await File.WriteAllTextAsync( + Path.Combine(optionsDirectory, "ToolBuildxBakeOptions.Generated.cs"), + "public record ToolBuildxBakeOptions;"); + await File.WriteAllTextAsync( + Path.Combine(optionsDirectory, "ToolBuilderBakeOptions.Generated.cs"), + "public record ToolBuilderBakeOptions : ToolBuildxBakeOptions;"); + var command = Command( + "ToolBuildxBakeOptions", + "ToolOptions", + ["buildx", "bake"], + subDomainGroup: "Buildx") with + { + Options = [RequiredOption("--source", "Source")], + }; + var tool = Tool(command) with + { + CommandGroupAliases = + [ + new CliCommandGroupAlias + { + Alias = "builder", + CanonicalCommand = "buildx", + ObsoleteMessage = "Use buildx instead.", + }, + ], + }; + + var preserved = GeneratedApiCompatibilityPreserver.Preserve(tool, root); + var generatedAlias = (await new OptionsClassGenerator().GenerateAsync(preserved)) + .Single(file => Path.GetFileName(file.RelativePath) + .Equals("ToolBuilderBakeOptions.Generated.cs", StringComparison.Ordinal)) + .Content; + + await Assert.That(generatedAlias).Contains("public ToolBuilderBakeOptions()"); + await Assert.That(generatedAlias).Contains(": this(default!)"); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + [Test] public async Task ApiCompatibilityPreserver_Rekeys_Documentation_Examples_After_Required_Rename() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index 181434d9032..f4e0c5464d5 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -51,6 +51,10 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi .Select(command => baseline.TryGetValue(command.ClassName, out var commandBaseline) ? Preserve(command, commandBaseline.Properties, commandBaseline.Constructors) : command) + .Select(command => PreserveAliasConstructors( + compatibleTool, + command, + baseline)) .Select(command => executeFacadeOptionTypes.Contains(command.ClassName) ? command with { PreserveExecuteFacade = true } : command) @@ -66,6 +70,61 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi return preservedTool; } + private static CliCommandDefinition PreserveAliasConstructors( + CliToolDefinition tool, + CliCommandDefinition command, + IReadOnlyDictionary baseline) + { + if (command.CommandParts.Length == 0) + { + return command; + } + + var constructorsByAlias = command.AliasCompatibilityConstructors + .ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal); + foreach (var alias in tool.CommandGroupAliases.Where(alias => + command.CommandParts[0].Equals( + alias.CanonicalCommand, + StringComparison.OrdinalIgnoreCase))) + { + var aliasClassName = GeneratorUtils.GetAliasedClassName(tool, alias, command.ClassName); + if (!baseline.TryGetValue(aliasClassName, out var aliasBaseline)) + { + continue; + } + + var compatibilityConstructors = constructorsByAlias + .GetValueOrDefault(aliasClassName, []) + .ToList(); + var currentRequired = GeneratorUtils.GetRequiredConstructorParameters(command) + .Select(parameter => new GeneratedApiProperty( + parameter.PropertyName, + GeneratorUtils.GetAliasedRequiredConstructorParameterType(parameter, tool, alias), + null, + null, + true, + false, + null, + null)) + .ToArray(); + PreserveCompatibilityConstructors( + aliasBaseline.Properties, + aliasBaseline.Constructors, + currentRequired, + compatibilityConstructors); + if (compatibilityConstructors.Count == 0) + { + constructorsByAlias.Remove(aliasClassName); + } + else + { + constructorsByAlias[aliasClassName] = compatibilityConstructors; + } + } + + return command with { AliasCompatibilityConstructors = constructorsByAlias }; + } + internal static CliToolDefinition PreserveGlobalOptions( CliToolDefinition tool, IReadOnlyList baselineProperties) @@ -488,12 +547,22 @@ private static void PreserveCompatibilityConstructors( IReadOnlyList baselineConstructors, IReadOnlyList positionalArguments, IReadOnlyList options, + List compatibilityConstructors) => + PreserveCompatibilityConstructors( + baselineProperties, + baselineConstructors, + GetCurrentProperties(positionalArguments, options) + .Where(static property => property.IsRequired) + .ToArray(), + compatibilityConstructors); + + private static void PreserveCompatibilityConstructors( + IReadOnlyList baselineProperties, + IReadOnlyList baselineConstructors, + IReadOnlyList currentRequired, List compatibilityConstructors) { - var currentRequired = GetCurrentProperties(positionalArguments, options) - .Where(static property => property.IsRequired) - .ToArray(); - if (currentRequired.Length == 0) + if (currentRequired.Count == 0) { compatibilityConstructors.Clear(); return; @@ -847,6 +916,27 @@ private static List ReadProperties( isRequired: true, accessorList: null))); } + else + { + var baseConstructor = declaration.Members + .OfType() + .FirstOrDefault(constructor => + constructor.Modifiers.Any(SyntaxKind.PublicKeyword) + && constructor.Initializer?.IsKind(SyntaxKind.BaseConstructorInitializer) == true); + if (baseConstructor is not null) + { + properties.AddRange(baseConstructor.ParameterList.Parameters.Select(parameter => + new GeneratedApiProperty( + parameter.Identifier.ValueText, + parameter.Type?.ToString() ?? string.Empty, + null, + null, + true, + false, + null, + null))); + } + } properties.AddRange(declaration.Members .OfType() diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs index 67b288cba21..6186702834f 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs @@ -1150,6 +1150,22 @@ internal static string GetAliasedClassName( + canonicalClassName[canonicalPrefix.Length..]; } + internal static string GetAliasedRequiredConstructorParameterType( + RequiredConstructorParameter parameter, + CliToolDefinition tool, + CliCommandGroupAlias alias) + { + var type = parameter.CSharpType.TrimEnd('?'); + var canonicalEnumName = parameter.Option?.EnumDefinition?.EnumName; + if (canonicalEnumName is null) + { + return type; + } + + var aliasEnumName = GetAliasedClassName(tool, alias, canonicalEnumName); + return type.Replace(canonicalEnumName, aliasEnumName, StringComparison.Ordinal); + } + internal static string GetCommandGroupIdentifier(CliCommandDefinition command) => command.CommandGroupIdentifierOverride ?? GenerateMethodNameFromCommandParts(command.CommandParts); diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs index 90c4c786647..521361e8c44 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs @@ -52,6 +52,8 @@ private static GeneratedFile GenerateCompatibilityOptionsAlias( tool, alias, command.ClassName); + var compatibilityConstructors = command.AliasCompatibilityConstructors + .GetValueOrDefault(aliasClassName, []); var requiredParameters = GeneratorUtils.GetRequiredConstructorParameters(command); var enumOptions = command.Options .Where(option => option.EnumDefinition is not null @@ -78,7 +80,9 @@ private static GeneratedFile GenerateCompatibilityOptionsAlias( sb.AppendLine(); sb.AppendLine(GeneratorUtils.GeneratedCodeAttribute); sb.AppendLine("[ExcludeFromCodeCoverage]"); - if (enumOptions.Length == 0 && requiredParameters.Count == 0) + if (enumOptions.Length == 0 + && requiredParameters.Count == 0 + && compatibilityConstructors.Count == 0) { sb.AppendLine($"public record {aliasClassName} : {command.ClassName};"); } @@ -92,6 +96,10 @@ private static GeneratedFile GenerateCompatibilityOptionsAlias( requiredParameters, tool, alias); + GenerateCompatibilityConstructors( + sb, + aliasClassName, + compatibilityConstructors); foreach (var option in enumOptions) { GenerateCompatibilityEnumProperty(sb, option, tool, alias); @@ -123,7 +131,7 @@ private static void GenerateCompatibilityConstructor( } var parameterDeclarations = requiredParameters.Select(parameter => - $" {GetCompatibilityParameterType(parameter, tool, alias)} {parameter.PropertyName}"); + $" {GeneratorUtils.GetAliasedRequiredConstructorParameterType(parameter, tool, alias)} {parameter.PropertyName}"); var baseArguments = requiredParameters.Select(parameter => GetCompatibilityBaseArgument(parameter, parameter.PropertyName)); sb.AppendLine($" public {aliasClassName}("); @@ -135,22 +143,6 @@ private static void GenerateCompatibilityConstructor( sb.AppendLine(); } - private static string GetCompatibilityParameterType( - GeneratorUtils.RequiredConstructorParameter parameter, - CliToolDefinition tool, - CliCommandGroupAlias alias) - { - var type = parameter.CSharpType.TrimEnd('?'); - var canonicalEnumName = parameter.Option?.EnumDefinition?.EnumName; - if (canonicalEnumName is null) - { - return type; - } - - var aliasEnumName = GeneratorUtils.GetAliasedClassName(tool, alias, canonicalEnumName); - return type.Replace(canonicalEnumName, aliasEnumName, StringComparison.Ordinal); - } - private static string GetCompatibilityBaseArgument( GeneratorUtils.RequiredConstructorParameter parameter, string parameterName) @@ -262,7 +254,10 @@ private static string GenerateOptionsClass(CliCommandDefinition command, CliTool var existingPropertyNames = GenerateClassDeclaration(sb, command, positionalArguments); sb.AppendLine("{"); - GenerateCompatibilityConstructors(sb, command); + GenerateCompatibilityConstructors( + sb, + command.ClassName, + command.CompatibilityConstructors); GenerateProperties(sb, command, positionalArguments, existingPropertyNames); sb.AppendLine("}"); @@ -271,13 +266,14 @@ private static string GenerateOptionsClass(CliCommandDefinition command, CliTool private static void GenerateCompatibilityConstructors( StringBuilder sb, - CliCommandDefinition command) + string className, + IReadOnlyList constructors) { - foreach (var constructor in command.CompatibilityConstructors) + foreach (var constructor in constructors) { var parameters = constructor.Parameters .Select(parameter => $"{parameter.CSharpType} {parameter.PropertyName}"); - sb.AppendLine($" public {command.ClassName}({string.Join(", ", parameters)})"); + sb.AppendLine($" public {className}({string.Join(", ", parameters)})"); sb.AppendLine($" : this({string.Join(", ", constructor.PrimaryConstructorArguments)})"); sb.AppendLine(" {"); sb.AppendLine(" }"); diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs index e7536800c39..15256becf47 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs @@ -97,6 +97,12 @@ public record CliCommandDefinition /// public IReadOnlyList CompatibilityConstructors { get; init; } = []; + /// + /// Secondary constructors retained on command-group alias option records, keyed by alias class name. + /// + public IReadOnlyDictionary> AliasCompatibilityConstructors + { get; init; } = new Dictionary>(StringComparer.Ordinal); + /// /// Whether an existing command-group ExecuteAsync facade must remain generated. /// From 2f61a98c8c3590b0ac657b9619a16ea1f0cd576c Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 16 Aug 2026 14:24:39 +0100 Subject: [PATCH 15/21] fix(generator): preserve generated enum compatibility (#3935) --- .../EnumDefinitionStabilizerTests.cs | 41 +++++ .../Generators/GeneratorHardeningTests.cs | 71 +++++++++ .../Generators/EnumDefinitionStabilizer.cs | 41 ++++- .../GeneratedApiCompatibilityPreserver.cs | 141 +++++++++++++++++- .../Generators/GeneratorUtils.cs | 9 ++ .../Generators/GlobalOptionsBaseGenerator.cs | 6 +- .../Generators/OptionsClassGenerator.cs | 77 +++++++++- .../Models/CliCommandDefinition.cs | 32 ++++ .../Models/CliToolDefinition.cs | 6 + 9 files changed, 412 insertions(+), 12 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/EnumDefinitionStabilizerTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/EnumDefinitionStabilizerTests.cs index 6b4a1f75707..fda406ea9ed 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/EnumDefinitionStabilizerTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/EnumDefinitionStabilizerTests.cs @@ -131,6 +131,47 @@ await Assert.That(stabilized.AllEnums.Single().Values.Single().MemberName) } } + [Test] + public async Task Stabilize_Retains_Removed_Members_And_Restores_Renamed_Members() + { + var outputRoot = Path.Combine(Path.GetTempPath(), "mp-enum-tests", Guid.NewGuid().ToString("N")); + var enumDirectory = Path.Combine(outputRoot, "src", "Fake", "Enums"); + Directory.CreateDirectory(enumDirectory); + File.WriteAllText( + Path.Combine(enumDirectory, "FakeVisibility.Generated.cs"), + "public enum FakeVisibility { " + + "[EnumValue(\"private\")] Private = 4, " + + "[EnumValue(\"public\")] Public = 9 }"); + + try + { + var tool = Tool( + new CliEnumValue { MemberName = "PrivateAccess", CliValue = "private" }, + Value("enterprise")); + + var stabilized = EnumDefinitionStabilizer.Stabilize(tool, outputRoot); + var values = stabilized.AllEnums.Single().Values; + + using (Assert.Multiple()) + { + await Assert.That(values.Select(value => value.MemberName)) + .IsEquivalentTo(["Private", "Public", "Enterprise"]); + await Assert.That(values.Single(value => value.MemberName == "Private").NumericValue) + .IsEqualTo(4); + await Assert.That(values.Single(value => value.MemberName == "Public").CliValue) + .IsEqualTo("public"); + await Assert.That(values.Single(value => value.MemberName == "Public").NumericValue) + .IsEqualTo(9); + await Assert.That(values.Single(value => value.MemberName == "Enterprise").NumericValue) + .IsEqualTo(10); + } + } + finally + { + Directory.Delete(outputRoot, recursive: true); + } + } + [Test] public async Task Stabilize_Rejects_Suspicious_Prose_Values() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs index 7fe31d197c6..e103545cc77 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs @@ -1568,6 +1568,77 @@ await File.WriteAllTextAsync( } } + [Test] + public async Task ApiCompatibilityPreserver_Retains_Command_Group_Alias_Enum_Properties() + { + var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}"); + var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool"); + var optionsDirectory = Path.Combine(packageDirectory, "Options"); + var enumsDirectory = Path.Combine(packageDirectory, "Enums"); + Directory.CreateDirectory(optionsDirectory); + Directory.CreateDirectory(enumsDirectory); + try + { + await File.WriteAllTextAsync( + Path.Combine(optionsDirectory, "ToolBuildxBakeOptions.Generated.cs"), + "public record ToolBuildxBakeOptions { " + + "[CliOption(\"--progress\")] public ToolBuildxBakeProgress? Progress { get; set; } }"); + await File.WriteAllTextAsync( + Path.Combine(optionsDirectory, "ToolBuilderBakeOptions.Generated.cs"), + "public record ToolBuilderBakeOptions : ToolBuildxBakeOptions { " + + "[CliOption(\"--progress\")] public new ToolBuilderBakeProgress? Progress { get; set; } }"); + await File.WriteAllTextAsync( + Path.Combine(enumsDirectory, "ToolBuildxBakeProgress.Generated.cs"), + "public enum ToolBuildxBakeProgress { [EnumValue(\"plain\")] Plain = 4 }"); + await File.WriteAllTextAsync( + Path.Combine(enumsDirectory, "ToolBuilderBakeProgress.Generated.cs"), + "public enum ToolBuilderBakeProgress { [EnumValue(\"plain\")] Plain = 4 }"); + var command = Command( + "ToolBuildxBakeOptions", + "ToolOptions", + ["buildx", "bake"], + subDomainGroup: "Buildx"); + var tool = Tool(command) with + { + CommandGroupAliases = + [ + new CliCommandGroupAlias + { + Alias = "builder", + CanonicalCommand = "buildx", + ObsoleteMessage = "Use buildx instead.", + }, + ], + }; + + var preserved = GeneratedApiCompatibilityPreserver.Preserve(tool, root); + var generatedOptions = await new OptionsClassGenerator().GenerateAsync(preserved); + var generatedAlias = generatedOptions.Single(file => Path.GetFileName(file.RelativePath) + .Equals("ToolBuilderBakeOptions.Generated.cs", StringComparison.Ordinal)).Content; + var generatedCanonical = generatedOptions.Single(file => Path.GetFileName(file.RelativePath) + .Equals("ToolBuildxBakeOptions.Generated.cs", StringComparison.Ordinal)).Content; + var generatedEnums = await new EnumGenerator().GenerateAsync(preserved); + + using (Assert.Multiple()) + { + await Assert.That(generatedCanonical) + .Contains("public ToolBuildxBakeProgress? Progress { get; set; }"); + await Assert.That(generatedAlias) + .Contains("public new ToolBuilderBakeProgress? Progress"); + await Assert.That(generatedAlias) + .Contains("(ToolBuildxBakeProgress)(int)value.Value"); + await Assert.That(generatedEnums.Select(file => Path.GetFileName(file.RelativePath))) + .Contains("ToolBuildxBakeProgress.Generated.cs"); + await Assert.That(generatedEnums.Select(file => Path.GetFileName(file.RelativePath))) + .Contains("ToolBuilderBakeProgress.Generated.cs"); + } + } + finally + { + Directory.Delete(root, recursive: true); + } + } + [Test] public async Task ApiCompatibilityPreserver_Rekeys_Documentation_Examples_After_Required_Rename() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs index 9fff6663789..b8d26622ccd 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs @@ -4,7 +4,7 @@ namespace ModularPipelines.OptionsGenerator.Generators; /// -/// Preserves the public numeric contract of generated enums across regeneration. +/// Preserves generated enum members, CLI values, and numeric values across regeneration. /// internal static partial class EnumDefinitionStabilizer { @@ -43,6 +43,9 @@ public static CliToolDefinition Stabilize( { Commands = commands, GlobalOptions = StabilizeOptions(tool.GlobalOptions, stabilizedEnums), + CompatibilityEnums = tool.CompatibilityEnums + .Select(definition => stabilizedEnums[definition.EnumName]) + .ToList(), }; } @@ -81,7 +84,25 @@ private static CliEnumDefinition Stabilize( MemberName = existingValue.MemberName, NumericValue = existingValue.NumericValue, }); + continue; } + + var reusedMember = definition.Values.FirstOrDefault(value => value.MemberName.Equals( + existingValue.MemberName, + StringComparison.Ordinal)); + if (reusedMember is not null) + { + throw new InvalidOperationException( + $"Enum '{definition.EnumName}' member '{existingValue.MemberName}' changed CLI value from " + + $"'{existingValue.CliValue}' to '{reusedMember.CliValue}'."); + } + + stabilizedValues.Add(new CliEnumValue + { + MemberName = existingValue.MemberName, + CliValue = existingValue.CliValue, + NumericValue = existingValue.NumericValue, + }); } var usedNumericValues = existingValues @@ -97,6 +118,15 @@ private static CliEnumDefinition Stabilize( for (var index = 0; index < newValues.Count; index++) { var incomingValue = newValues[index]; + if (stabilizedValues.Any(value => value.MemberName.Equals( + incomingValue.MemberName, + StringComparison.Ordinal))) + { + throw new InvalidOperationException( + $"Enum '{definition.EnumName}' would generate duplicate member " + + $"'{incomingValue.MemberName}'."); + } + while (usedNumericValues.Contains(nextNumericValue)) { nextNumericValue = checked(nextNumericValue + 1); @@ -125,6 +155,15 @@ private static void ValidateValues(CliEnumDefinition definition) $"Enum '{definition.EnumName}' contains duplicate CLI value '{duplicateCliValue.Key}'."); } + var duplicateMember = definition.Values + .GroupBy(value => value.MemberName, StringComparer.Ordinal) + .FirstOrDefault(group => group.Count() > 1); + if (duplicateMember is not null) + { + throw new InvalidOperationException( + $"Enum '{definition.EnumName}' contains duplicate member '{duplicateMember.Key}'."); + } + var suspiciousValue = definition.Values .FirstOrDefault(value => SuspiciousProseValues.Contains(value.CliValue)); if (suspiciousValue is not null) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index f4e0c5464d5..cd52f55e68d 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using ModularPipelines.Attributes; using ModularPipelines.OptionsGenerator.Models; namespace ModularPipelines.OptionsGenerator.Generators; @@ -25,6 +26,17 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi return tool; } + var enumBaseline = ReadEnumBaseline(Path.Combine( + outputDirectory, + tool.OutputDirectory, + "Enums")); + tool = tool with + { + CompatibilityEnums = tool.CompatibilityEnums + .Concat(enumBaseline.Values) + .DistinctBy(static definition => definition.EnumName) + .ToArray(), + }; var baseline = ReadBaseline(optionsDirectory); var compatibleTool = baseline.TryGetValue($"{tool.NamespacePrefix}Options", out var globalBaseline) ? PreserveGlobalOptions(tool, globalBaseline.Properties) @@ -51,10 +63,11 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi .Select(command => baseline.TryGetValue(command.ClassName, out var commandBaseline) ? Preserve(command, commandBaseline.Properties, commandBaseline.Constructors) : command) - .Select(command => PreserveAliasConstructors( + .Select(command => PreserveAliasCompatibility( compatibleTool, command, - baseline)) + baseline, + enumBaseline)) .Select(command => executeFacadeOptionTypes.Contains(command.ClassName) ? command with { PreserveExecuteFacade = true } : command) @@ -70,10 +83,11 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi return preservedTool; } - private static CliCommandDefinition PreserveAliasConstructors( + private static CliCommandDefinition PreserveAliasCompatibility( CliToolDefinition tool, CliCommandDefinition command, - IReadOnlyDictionary baseline) + IReadOnlyDictionary baseline, + IReadOnlyDictionary enumBaseline) { if (command.CommandParts.Length == 0) { @@ -82,6 +96,8 @@ private static CliCommandDefinition PreserveAliasConstructors( var constructorsByAlias = command.AliasCompatibilityConstructors .ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal); + var propertiesByAlias = command.AliasCompatibilityProperties + .ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal); foreach (var alias in tool.CommandGroupAliases.Where(alias => command.CommandParts[0].Equals( alias.CanonicalCommand, @@ -120,9 +136,71 @@ private static CliCommandDefinition PreserveAliasConstructors( { constructorsByAlias[aliasClassName] = compatibilityConstructors; } + + var currentAliasProperties = command.Options + .Where(static option => option.EnumDefinition is not null + && option.ValueArity != CliOptionValueArity.Optional) + .Select(option => ( + option.PropertyName, + option.CSharpType.Replace( + option.EnumDefinition!.EnumName, + GeneratorUtils.GetAliasedClassName( + tool, + alias, + option.EnumDefinition.EnumName), + StringComparison.Ordinal))) + .ToHashSet(); + var compatibilityProperties = propertiesByAlias + .GetValueOrDefault(aliasClassName, []) + .ToList(); + var canonicalProperties = baseline.GetValueOrDefault(command.ClassName)?.Properties ?? []; + foreach (var baselineProperty in aliasBaseline.Properties) + { + var aliasEnumName = GeneratorUtils.GetEnumTypeName(baselineProperty.CSharpType); + if (!enumBaseline.ContainsKey(aliasEnumName) + || currentAliasProperties.Contains(( + baselineProperty.PropertyName, + baselineProperty.CSharpType)) + || compatibilityProperties.Any(existing => existing.PropertyName.Equals( + baselineProperty.PropertyName, + StringComparison.Ordinal))) + { + continue; + } + + var canonicalProperty = canonicalProperties.FirstOrDefault(property => + property.PropertyName.Equals(baselineProperty.PropertyName, StringComparison.Ordinal)); + if (canonicalProperty is null + || !enumBaseline.ContainsKey(GeneratorUtils.GetEnumTypeName(canonicalProperty.CSharpType))) + { + continue; + } + + compatibilityProperties.Add(new CliAliasCompatibilityProperty + { + PropertyName = baselineProperty.PropertyName, + AliasCSharpType = baselineProperty.CSharpType, + CanonicalCSharpType = canonicalProperty.CSharpType, + ObsoleteMessage = baselineProperty.ObsoleteMessage + ?? $"{baselineProperty.PropertyName} is retained for compatibility.", + }); + } + + if (compatibilityProperties.Count == 0) + { + propertiesByAlias.Remove(aliasClassName); + } + else + { + propertiesByAlias[aliasClassName] = compatibilityProperties; + } } - return command with { AliasCompatibilityConstructors = constructorsByAlias }; + return command with + { + AliasCompatibilityConstructors = constructorsByAlias, + AliasCompatibilityProperties = propertiesByAlias, + }; } internal static CliToolDefinition PreserveGlobalOptions( @@ -759,6 +837,59 @@ private static Dictionary ReadBaseline( return baseline; } + private static Dictionary ReadEnumBaseline(string enumsDirectory) + { + var baseline = new Dictionary(StringComparer.Ordinal); + if (!Directory.Exists(enumsDirectory)) + { + return baseline; + } + + foreach (var path in Directory.EnumerateFiles( + enumsDirectory, + "*.Generated.cs", + SearchOption.TopDirectoryOnly)) + { + var root = CSharpSyntaxTree.ParseText(File.ReadAllText(path)).GetRoot(); + foreach (var declaration in root.DescendantNodes().OfType()) + { + var nextNumericValue = 0; + var values = new List(declaration.Members.Count); + foreach (var member in declaration.Members) + { + var attributes = member.AttributeLists.SelectMany(static list => list.Attributes); + var cliValueAttribute = FindAttribute(attributes, "EnumValue") + ?? FindAttribute(attributes, "Description"); + var numericValue = GetEnumNumericValue(member.EqualsValue?.Value) ?? nextNumericValue; + values.Add(new CliEnumValue + { + MemberName = member.Identifier.ValueText, + CliValue = GetStringArgument(cliValueAttribute) ?? member.Identifier.ValueText, + NumericValue = numericValue, + }); + nextNumericValue = checked(numericValue + 1); + } + + baseline[declaration.Identifier.ValueText] = new CliEnumDefinition + { + EnumName = declaration.Identifier.ValueText, + Values = values, + }; + } + } + + return baseline; + } + + private static int? GetEnumNumericValue(ExpressionSyntax? expression) => expression switch + { + LiteralExpressionSyntax literal when literal.Token.Value is int value => value, + PrefixUnaryExpressionSyntax prefix when prefix.IsKind(SyntaxKind.UnaryMinusExpression) + && prefix.Operand is LiteralExpressionSyntax literal + && literal.Token.Value is int value => -value, + _ => null, + }; + private static CliCompatibilityConstructor[] ReadCompatibilityConstructors( RecordDeclarationSyntax declaration) => declaration.Members diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs index 6186702834f..ac3d14f9ebe 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs @@ -14,6 +14,15 @@ namespace ModularPipelines.OptionsGenerator.Generators; /// public static partial class GeneratorUtils { + internal static string GetEnumTypeName(string cSharpType) + { + var type = cSharpType.TrimEnd('?'); + const string enumerablePrefix = "IEnumerable<"; + return type.StartsWith(enumerablePrefix, StringComparison.Ordinal) + ? type[enumerablePrefix.Length..^1] + : type; + } + private static readonly string[] KnownRunnerHomeDirectories = [ "/home/runner", diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs index cf56715c477..829c06c3b35 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs @@ -53,7 +53,11 @@ private static string GenerateBaseOptionsClass(CliToolDefinition tool) sb.AppendLine("using System.ComponentModel.DataAnnotations;"); } - if (globalOptions.Any(o => o.EnumDefinition is not null)) + if (globalOptions.Any(o => o.EnumDefinition is not null) + || tool.GlobalCompatibilityProperties.Any(property => tool.AllEnums.Any(definition => + definition.EnumName.Equals( + GeneratorUtils.GetEnumTypeName(property.CSharpType), + StringComparison.Ordinal)))) { sb.AppendLine($"using {tool.TargetNamespace}.Enums;"); } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs index 521361e8c44..e1b52c9b457 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs @@ -54,10 +54,16 @@ private static GeneratedFile GenerateCompatibilityOptionsAlias( command.ClassName); var compatibilityConstructors = command.AliasCompatibilityConstructors .GetValueOrDefault(aliasClassName, []); + var compatibilityProperties = command.AliasCompatibilityProperties + .GetValueOrDefault(aliasClassName, []); + var compatibilityPropertyNames = compatibilityProperties + .Select(static property => property.PropertyName) + .ToHashSet(StringComparer.Ordinal); var requiredParameters = GeneratorUtils.GetRequiredConstructorParameters(command); var enumOptions = command.Options .Where(option => option.EnumDefinition is not null - && option.ValueArity != CliOptionValueArity.Optional) + && option.ValueArity != CliOptionValueArity.Optional + && !compatibilityPropertyNames.Contains(option.PropertyName)) .ToArray(); var sb = new StringBuilder(); GeneratorUtils.GenerateFileHeaderWithNullable(sb, command.DocumentationUrl); @@ -69,9 +75,13 @@ private static GeneratedFile GenerateCompatibilityOptionsAlias( sb.AppendLine("using ModularPipelines.Models;"); } - if (enumOptions.Length > 0) + if (enumOptions.Length > 0 || compatibilityProperties.Count > 0) { - sb.AppendLine("using ModularPipelines.Attributes;"); + if (enumOptions.Length > 0) + { + sb.AppendLine("using ModularPipelines.Attributes;"); + } + sb.AppendLine($"using {tool.TargetNamespace}.Enums;"); } @@ -82,7 +92,8 @@ private static GeneratedFile GenerateCompatibilityOptionsAlias( sb.AppendLine("[ExcludeFromCodeCoverage]"); if (enumOptions.Length == 0 && requiredParameters.Count == 0 - && compatibilityConstructors.Count == 0) + && compatibilityConstructors.Count == 0 + && compatibilityProperties.Count == 0) { sb.AppendLine($"public record {aliasClassName} : {command.ClassName};"); } @@ -105,6 +116,11 @@ private static GeneratedFile GenerateCompatibilityOptionsAlias( GenerateCompatibilityEnumProperty(sb, option, tool, alias); } + foreach (var property in compatibilityProperties) + { + GenerateAliasCompatibilityProperty(sb, property); + } + sb.AppendLine("}"); } @@ -229,6 +245,53 @@ private static void GenerateCompatibilityEnumCollectionAccessors( + $"static value => ({canonicalEnumName})(int)value);"); } + private static void GenerateAliasCompatibilityProperty( + StringBuilder sb, + CliAliasCompatibilityProperty property) + { + var aliasEnumName = GeneratorUtils.GetEnumTypeName(property.AliasCSharpType); + var canonicalEnumName = GeneratorUtils.GetEnumTypeName(property.CanonicalCSharpType); + var aliasIsEnumerable = property.AliasCSharpType.StartsWith("IEnumerable<", StringComparison.Ordinal); + var canonicalIsEnumerable = property.CanonicalCSharpType.StartsWith("IEnumerable<", StringComparison.Ordinal); + if (aliasIsEnumerable != canonicalIsEnumerable) + { + throw new InvalidOperationException( + $"Cannot retain alias property {property.PropertyName} because its collection shape changed."); + } + + sb.AppendLine($" [Obsolete({GeneratorUtils.FormatStringLiteral(property.ObsoleteMessage)})]"); + sb.AppendLine($" public new {property.AliasCSharpType} {property.PropertyName}"); + sb.AppendLine(" {"); + if (aliasIsEnumerable) + { + var baseNullableOperator = property.CanonicalCSharpType.EndsWith('?') ? "?" : string.Empty; + var aliasNullableOperator = property.AliasCSharpType.EndsWith('?') ? "?" : string.Empty; + sb.AppendLine( + $" get => base.{property.PropertyName}{baseNullableOperator}.Select(" + + $"static value => ({aliasEnumName})(int)value);"); + sb.AppendLine( + $" set => base.{property.PropertyName} = value{aliasNullableOperator}.Select(" + + $"static value => ({canonicalEnumName})(int)value);"); + } + else if (property.AliasCSharpType.EndsWith('?') + && property.CanonicalCSharpType.EndsWith('?')) + { + sb.AppendLine($" get => base.{property.PropertyName} is null"); + sb.AppendLine(" ? null"); + sb.AppendLine($" : ({aliasEnumName})(int)base.{property.PropertyName}.Value;"); + sb.AppendLine($" set => base.{property.PropertyName} = value is null"); + sb.AppendLine(" ? null"); + sb.AppendLine($" : ({canonicalEnumName})(int)value.Value;"); + } + else + { + sb.AppendLine($" get => ({aliasEnumName})(int)base.{property.PropertyName};"); + sb.AppendLine($" set => base.{property.PropertyName} = ({canonicalEnumName})(int)value;"); + } + + sb.AppendLine(" }"); + } + private static string GenerateOptionsClass(CliCommandDefinition command, CliToolDefinition tool) { var sb = new StringBuilder(); @@ -316,7 +379,11 @@ private static void GenerateUsings(StringBuilder sb, CliCommandDefinition comman } // Include enums namespace if any options use enum types - if (command.Options.Any(o => o.EnumDefinition is not null)) + if (command.Options.Any(o => o.EnumDefinition is not null) + || command.CompatibilityProperties.Any(property => tool.AllEnums.Any(definition => + definition.EnumName.Equals( + GeneratorUtils.GetEnumTypeName(property.CSharpType), + StringComparison.Ordinal)))) { sb.AppendLine($"using {tool.TargetNamespace}.Enums;"); } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs index 15256becf47..984e7ebf029 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs @@ -103,6 +103,12 @@ public record CliCommandDefinition public IReadOnlyDictionary> AliasCompatibilityConstructors { get; init; } = new Dictionary>(StringComparer.Ordinal); + /// + /// Enum-valued properties retained on command-group alias records, keyed by alias class name. + /// + public IReadOnlyDictionary> AliasCompatibilityProperties + { get; init; } = new Dictionary>(StringComparer.Ordinal); + /// /// Whether an existing command-group ExecuteAsync facade must remain generated. /// @@ -238,6 +244,32 @@ public record CliCompatibilityConstructor /// public readonly record struct CliCompatibilityConstructorParameter(string PropertyName, string CSharpType); +/// +/// Describes an enum-valued property retained on a command-group alias record. +/// +public record CliAliasCompatibilityProperty +{ + /// + /// CLR property name shared with the canonical options record. + /// + public required string PropertyName { get; init; } + + /// + /// Previously generated alias enum type exposed to consumers. + /// + public required string AliasCSharpType { get; init; } + + /// + /// Canonical enum type used by the base options record. + /// + public required string CanonicalCSharpType { get; init; } + + /// + /// Obsolete diagnostic shown to consumers. + /// + public required string ObsoleteMessage { get; init; } +} + /// /// Describes an obsolete generated method retained as a forwarding alias. /// diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliToolDefinition.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliToolDefinition.cs index 19e84080693..81562609364 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliToolDefinition.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliToolDefinition.cs @@ -129,9 +129,15 @@ public IReadOnlyList GetGlobalOptions() => .Concat(GetGlobalOptions() .Where(option => option.EnumDefinition is not null) .Select(option => option.EnumDefinition!)) + .Concat(CompatibilityEnums) .DistinctBy(e => e.EnumName) .ToList(); + /// + /// Generated enum types retained after the installed CLI stops advertising them. + /// + public IReadOnlyList CompatibilityEnums { get; init; } = []; + /// /// Any scraping errors encountered. /// From 9c8aecdd601e3a2dc28ff109827c4d9d44ec6c3a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:48:27 +0100 Subject: [PATCH 16/21] fix(generator): preserve merged enum validation --- .../Generators/EnumDefinitionStabilizer.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs index b8d26622ccd..57d62c900ba 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs @@ -118,15 +118,6 @@ private static CliEnumDefinition Stabilize( for (var index = 0; index < newValues.Count; index++) { var incomingValue = newValues[index]; - if (stabilizedValues.Any(value => value.MemberName.Equals( - incomingValue.MemberName, - StringComparison.Ordinal))) - { - throw new InvalidOperationException( - $"Enum '{definition.EnumName}' would generate duplicate member " - + $"'{incomingValue.MemberName}'."); - } - while (usedNumericValues.Contains(nextNumericValue)) { nextNumericValue = checked(nextNumericValue + 1); From d9e9b6fd9ed54b121804d1e90b8f2364962c62ac Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:53:00 +0100 Subject: [PATCH 17/21] refactor(generator): reduce compatibility complexity --- .../Generators/EnumDefinitionStabilizer.cs | 36 ++- .../GeneratedApiCompatibilityPreserver.cs | 221 +++++++++++------- 2 files changed, 168 insertions(+), 89 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs index 57d62c900ba..987b195c4c6 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/EnumDefinitionStabilizer.cs @@ -64,14 +64,30 @@ private static CliEnumDefinition Stabilize( { ValidateValues(definition); + var existingValues = ReadExistingValues(existingFile, fallbackExistingFile); + var stabilizedValues = PreserveExistingValues(definition, existingValues); + AppendNewValues(definition, existingValues, stabilizedValues); + + ValidateUniqueMemberNames(definition.EnumName, stabilizedValues); + return definition with { Values = stabilizedValues }; + } + + private static IReadOnlyList ReadExistingValues( + string existingFile, + string? fallbackExistingFile) + { var baselineFile = File.Exists(existingFile) ? existingFile : fallbackExistingFile; - var existingValues = baselineFile is not null && File.Exists(baselineFile) + return baselineFile is not null && File.Exists(baselineFile) ? ParseExistingValues(File.ReadAllText(baselineFile)) : []; + } - var existingByCliValue = existingValues.ToDictionary(value => value.CliValue, StringComparer.Ordinal); + private static List PreserveExistingValues( + CliEnumDefinition definition, + IReadOnlyList existingValues) + { var incomingByCliValue = definition.Values.ToDictionary(value => value.CliValue, StringComparer.Ordinal); var stabilizedValues = new List(definition.Values.Count); @@ -105,11 +121,22 @@ private static CliEnumDefinition Stabilize( }); } + return stabilizedValues; + } + + private static void AppendNewValues( + CliEnumDefinition definition, + IReadOnlyList existingValues, + List stabilizedValues) + { + var existingCliValues = existingValues + .Select(value => value.CliValue) + .ToHashSet(StringComparer.Ordinal); var usedNumericValues = existingValues .Select(value => value.NumericValue) .ToHashSet(); var newValues = definition.Values - .Where(value => !existingByCliValue.ContainsKey(value.CliValue)) + .Where(value => !existingCliValues.Contains(value.CliValue)) .ToList(); var nextNumericValue = newValues.Count > 0 && usedNumericValues.Count > 0 ? checked(usedNumericValues.Max() + 1) @@ -130,9 +157,6 @@ private static CliEnumDefinition Stabilize( nextNumericValue = checked(nextNumericValue + 1); } } - - ValidateUniqueMemberNames(definition.EnumName, stabilizedValues); - return definition with { Values = stabilizedValues }; } private static void ValidateValues(CliEnumDefinition definition) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index cd52f55e68d..9f68ecefd9a 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -109,100 +109,155 @@ private static CliCommandDefinition PreserveAliasCompatibility( continue; } - var compatibilityConstructors = constructorsByAlias - .GetValueOrDefault(aliasClassName, []) - .ToList(); - var currentRequired = GeneratorUtils.GetRequiredConstructorParameters(command) - .Select(parameter => new GeneratedApiProperty( - parameter.PropertyName, - GeneratorUtils.GetAliasedRequiredConstructorParameterType(parameter, tool, alias), - null, - null, - true, - false, - null, - null)) - .ToArray(); - PreserveCompatibilityConstructors( - aliasBaseline.Properties, - aliasBaseline.Constructors, - currentRequired, - compatibilityConstructors); - if (compatibilityConstructors.Count == 0) - { - constructorsByAlias.Remove(aliasClassName); - } - else - { - constructorsByAlias[aliasClassName] = compatibilityConstructors; - } - - var currentAliasProperties = command.Options - .Where(static option => option.EnumDefinition is not null - && option.ValueArity != CliOptionValueArity.Optional) - .Select(option => ( - option.PropertyName, - option.CSharpType.Replace( - option.EnumDefinition!.EnumName, - GeneratorUtils.GetAliasedClassName( - tool, - alias, - option.EnumDefinition.EnumName), - StringComparison.Ordinal))) - .ToHashSet(); - var compatibilityProperties = propertiesByAlias - .GetValueOrDefault(aliasClassName, []) - .ToList(); - var canonicalProperties = baseline.GetValueOrDefault(command.ClassName)?.Properties ?? []; - foreach (var baselineProperty in aliasBaseline.Properties) - { - var aliasEnumName = GeneratorUtils.GetEnumTypeName(baselineProperty.CSharpType); - if (!enumBaseline.ContainsKey(aliasEnumName) - || currentAliasProperties.Contains(( - baselineProperty.PropertyName, - baselineProperty.CSharpType)) - || compatibilityProperties.Any(existing => existing.PropertyName.Equals( - baselineProperty.PropertyName, - StringComparison.Ordinal))) - { - continue; - } + PreserveAliasConstructors( + tool, + command, + alias, + aliasClassName, + aliasBaseline, + constructorsByAlias); + PreserveAliasProperties( + tool, + command, + alias, + aliasClassName, + aliasBaseline, + baseline, + enumBaseline, + propertiesByAlias); + } - var canonicalProperty = canonicalProperties.FirstOrDefault(property => - property.PropertyName.Equals(baselineProperty.PropertyName, StringComparison.Ordinal)); - if (canonicalProperty is null - || !enumBaseline.ContainsKey(GeneratorUtils.GetEnumTypeName(canonicalProperty.CSharpType))) - { - continue; - } + return command with + { + AliasCompatibilityConstructors = constructorsByAlias, + AliasCompatibilityProperties = propertiesByAlias, + }; + } - compatibilityProperties.Add(new CliAliasCompatibilityProperty - { - PropertyName = baselineProperty.PropertyName, - AliasCSharpType = baselineProperty.CSharpType, - CanonicalCSharpType = canonicalProperty.CSharpType, - ObsoleteMessage = baselineProperty.ObsoleteMessage - ?? $"{baselineProperty.PropertyName} is retained for compatibility.", - }); - } + private static void PreserveAliasConstructors( + CliToolDefinition tool, + CliCommandDefinition command, + CliCommandGroupAlias alias, + string aliasClassName, + GeneratedApiBaseline aliasBaseline, + Dictionary> constructorsByAlias) + { + var compatibilityConstructors = constructorsByAlias + .GetValueOrDefault(aliasClassName, []) + .ToList(); + var currentRequired = GeneratorUtils.GetRequiredConstructorParameters(command) + .Select(parameter => new GeneratedApiProperty( + parameter.PropertyName, + GeneratorUtils.GetAliasedRequiredConstructorParameterType(parameter, tool, alias), + null, + null, + true, + false, + null, + null)) + .ToArray(); + PreserveCompatibilityConstructors( + aliasBaseline.Properties, + aliasBaseline.Constructors, + currentRequired, + compatibilityConstructors); + SetCompatibilityEntries(constructorsByAlias, aliasClassName, compatibilityConstructors); + } - if (compatibilityProperties.Count == 0) - { - propertiesByAlias.Remove(aliasClassName); - } - else + private static void PreserveAliasProperties( + CliToolDefinition tool, + CliCommandDefinition command, + CliCommandGroupAlias alias, + string aliasClassName, + GeneratedApiBaseline aliasBaseline, + IReadOnlyDictionary baseline, + IReadOnlyDictionary enumBaseline, + Dictionary> propertiesByAlias) + { + var currentAliasProperties = command.Options + .Where(static option => option.EnumDefinition is not null + && option.ValueArity != CliOptionValueArity.Optional) + .Select(option => ( + option.PropertyName, + option.CSharpType.Replace( + option.EnumDefinition!.EnumName, + GeneratorUtils.GetAliasedClassName( + tool, + alias, + option.EnumDefinition.EnumName), + StringComparison.Ordinal))) + .ToHashSet(); + var compatibilityProperties = propertiesByAlias + .GetValueOrDefault(aliasClassName, []) + .ToList(); + var canonicalProperties = baseline.GetValueOrDefault(command.ClassName)?.Properties ?? []; + + foreach (var baselineProperty in aliasBaseline.Properties) + { + var compatibilityProperty = CreateAliasCompatibilityProperty( + baselineProperty, + canonicalProperties, + enumBaseline, + currentAliasProperties, + compatibilityProperties); + if (compatibilityProperty is not null) { - propertiesByAlias[aliasClassName] = compatibilityProperties; + compatibilityProperties.Add(compatibilityProperty); } } - return command with + SetCompatibilityEntries(propertiesByAlias, aliasClassName, compatibilityProperties); + } + + private static CliAliasCompatibilityProperty? CreateAliasCompatibilityProperty( + GeneratedApiProperty baselineProperty, + IReadOnlyList canonicalProperties, + IReadOnlyDictionary enumBaseline, + IReadOnlySet<(string PropertyName, string CSharpType)> currentAliasProperties, + IReadOnlyCollection compatibilityProperties) + { + var aliasEnumName = GeneratorUtils.GetEnumTypeName(baselineProperty.CSharpType); + if (!enumBaseline.ContainsKey(aliasEnumName) + || currentAliasProperties.Contains((baselineProperty.PropertyName, baselineProperty.CSharpType)) + || compatibilityProperties.Any(existing => existing.PropertyName.Equals( + baselineProperty.PropertyName, + StringComparison.Ordinal))) { - AliasCompatibilityConstructors = constructorsByAlias, - AliasCompatibilityProperties = propertiesByAlias, + return null; + } + + var canonicalProperty = canonicalProperties.FirstOrDefault(property => + property.PropertyName.Equals(baselineProperty.PropertyName, StringComparison.Ordinal)); + if (canonicalProperty is null + || !enumBaseline.ContainsKey(GeneratorUtils.GetEnumTypeName(canonicalProperty.CSharpType))) + { + return null; + } + + return new CliAliasCompatibilityProperty + { + PropertyName = baselineProperty.PropertyName, + AliasCSharpType = baselineProperty.CSharpType, + CanonicalCSharpType = canonicalProperty.CSharpType, + ObsoleteMessage = baselineProperty.ObsoleteMessage + ?? $"{baselineProperty.PropertyName} is retained for compatibility.", }; } + private static void SetCompatibilityEntries( + Dictionary> entriesByAlias, + string aliasClassName, + IReadOnlyList entries) + { + if (entries.Count == 0) + { + entriesByAlias.Remove(aliasClassName); + return; + } + + entriesByAlias[aliasClassName] = entries; + } + internal static CliToolDefinition PreserveGlobalOptions( CliToolDefinition tool, IReadOnlyList baselineProperties) From 6c4486fc8d5ed3dbe69fe90e57c3552bca9fac5c Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 17 Aug 2026 01:42:54 +0100 Subject: [PATCH 18/21] fix(generator): harden compatibility preservation --- .../Generators/GeneratorHardeningTests.cs | 214 ++++++++++++++---- .../Scrapers/DockerCliCompatibilityTests.cs | 18 ++ .../GeneratedApiCompatibilityPreserver.cs | 102 +++++++-- .../Generators/GeneratorUtils.cs | 3 +- .../Generators/OptionsClassGenerator.cs | 15 +- .../Models/CliCommandDefinition.cs | 7 +- .../Scrapers/Cli/CobraCliScraper.cs | 2 +- 7 files changed, 288 insertions(+), 73 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs index e103545cc77..184baacfa93 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs @@ -863,6 +863,7 @@ public async Task ApiCompatibilityPreserver_Restores_Required_Positional_Names() await Assert.That(generated).Contains("IEnumerable DepVersion"); await Assert.That(generated).Contains("public IEnumerable Dep"); await Assert.That(generated).Contains("get => DepVersion;"); + await Assert.That(generated).Contains("init => DepVersion = value;"); } } @@ -1030,7 +1031,7 @@ await Assert.That(generated.IndexOf("string First", StringComparison.Ordinal)) } [Test] - public async Task ApiCompatibilityPreserver_Preserves_Old_Deconstruct_Arity() + public async Task ApiCompatibilityPreserver_Rejects_Old_Deconstruct_Arity_For_New_Required_Members() { var command = Command("ToolMoveOptions", "ToolOptions", ["move"]) with { @@ -1053,16 +1054,12 @@ public async Task ApiCompatibilityPreserver_Preserves_Old_Deconstruct_Arity() ], }; - var preserved = GeneratedApiCompatibilityPreserver.Preserve( - command, - [BaselineProperty("Source", "string", switchName: "--source", isRequired: true)]); - var generated = (await new OptionsClassGenerator().GenerateAsync(Tool(preserved))).Single().Content; + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("Source", "string", switchName: "--source", isRequired: true)])); - using (Assert.Multiple()) - { - await Assert.That(generated).Contains("public void Deconstruct(out string Source)"); - await Assert.That(generated).Contains("Source = this.Source;"); - } + await Assert.That(exception.Message).Contains("newly required member(s) Destination have no baseline value"); } [Test] @@ -1118,7 +1115,7 @@ await Assert.That(exception.Message) } [Test] - public async Task ApiCompatibilityPreserver_Preserves_Parameterless_Constructor_When_Required_Member_Is_Added() + public async Task ApiCompatibilityPreserver_Rejects_Constructor_Preservation_When_Required_Member_Is_Added() { var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with { @@ -1134,11 +1131,10 @@ public async Task ApiCompatibilityPreserver_Preserves_Parameterless_Constructor_ ], }; - var preserved = GeneratedApiCompatibilityPreserver.Preserve(command, []); - var generated = (await new OptionsClassGenerator().GenerateAsync(Tool(preserved))).Single().Content; + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve(command, [])); - await Assert.That(generated).Contains("public ToolAddOptions()"); - await Assert.That(generated).Contains(": this(default!)"); + await Assert.That(exception.Message).Contains("newly required member(s) Package have no baseline value"); } [Test] @@ -1180,7 +1176,44 @@ await File.WriteAllTextAsync( } [Test] - public async Task ApiCompatibilityPreserver_Retains_Previously_Generated_Deconstruct_Overloads() + public async Task ApiCompatibilityPreserver_Does_Not_Retain_Nullability_Only_Constructor_Duplicates() + { + var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}"); + var optionsDirectory = Path.Combine(root, "src", "ModularPipelines.Tool", "Options"); + Directory.CreateDirectory(optionsDirectory); + try + { + await File.WriteAllTextAsync( + Path.Combine(optionsDirectory, "ToolAddOptions.Generated.cs"), + "public record ToolAddOptions([property: CliArgument(0)] string Package) " + + "{ public ToolAddOptions(string? LegacyPackage) : this(LegacyPackage!) { } }"); + var command = Command("ToolAddOptions", "ToolOptions", ["add"]) with + { + PositionalArguments = + [ + new CliPositionalArgument + { + PropertyName = "Package", + CSharpType = "string", + IsRequired = true, + PositionIndex = 0, + }, + ], + }; + + var preserved = GeneratedApiCompatibilityPreserver.Preserve(Tool(command), root); + var generated = (await new OptionsClassGenerator().GenerateAsync(preserved)).Single().Content; + + await Assert.That(generated).DoesNotContain("public ToolAddOptions(string? LegacyPackage)"); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Deconstruct_Preservation_For_New_Required_Members() { var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}"); var optionsDirectory = Path.Combine(root, "src", "ModularPipelines.Tool", "Options"); @@ -1203,10 +1236,10 @@ await File.WriteAllTextAsync( ], }; - var preserved = GeneratedApiCompatibilityPreserver.Preserve(Tool(command), root); - var generated = (await new OptionsClassGenerator().GenerateAsync(preserved)).Single().Content; + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve(Tool(command), root)); - await Assert.That(generated).Contains("public void Deconstruct(out string Source)"); + await Assert.That(exception.Message).Contains("newly required member(s) Force have no baseline value"); } finally { @@ -1288,7 +1321,7 @@ await Assert.That(exception.Message) } [Test] - public async Task ApiCompatibilityPreserver_Retains_Optional_Facade_When_Required_Member_Is_Added() + public async Task ApiCompatibilityPreserver_Rejects_Optional_Facade_When_Required_Member_Is_Added() { var root = Path.Combine(Path.GetTempPath(), $"service-api-{Guid.NewGuid():N}"); var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool"); @@ -1317,17 +1350,10 @@ await File.WriteAllTextAsync( ], }; - var preserved = GeneratedApiCompatibilityPreserver.Preserve(Tool(command), root); - var generatedOptions = (await new OptionsClassGenerator().GenerateAsync(preserved)).Single().Content; - var generatedInterface = (await new ServiceInterfaceGenerator().GenerateAsync(preserved)).Single().Content; - var generatedImplementation = (await new ServiceImplementationGenerator().GenerateAsync(preserved)).Single().Content; + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve(Tool(command), root)); - using (Assert.Multiple()) - { - await Assert.That(generatedOptions).Contains("public ToolAddOptions()"); - await Assert.That(generatedInterface).Contains("AddAsync(ToolAddOptions? options = null"); - await Assert.That(generatedImplementation).Contains("options ?? new ToolAddOptions()"); - } + await Assert.That(exception.Message).Contains("newly required member(s) Package have no baseline value"); } finally { @@ -1462,7 +1488,7 @@ await File.WriteAllTextAsync( } [Test] - public async Task ApiCompatibilityPreserver_Retains_Command_Group_Alias_Constructors() + public async Task ApiCompatibilityPreserver_Rejects_Alias_Constructors_For_New_Required_Members() { var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}"); var optionsDirectory = Path.Combine(root, "src", "ModularPipelines.Tool", "Options"); @@ -1501,16 +1527,10 @@ await File.WriteAllTextAsync( ], }; - var preserved = GeneratedApiCompatibilityPreserver.Preserve(tool, root); - var generatedAlias = (await new OptionsClassGenerator().GenerateAsync(preserved)) - .Single(file => Path.GetFileName(file.RelativePath) - .Equals("ToolBuilderBakeOptions.Generated.cs", StringComparison.Ordinal)) - .Content; + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve(tool, root)); - await Assert.That(generatedAlias) - .Contains("public ToolBuilderBakeOptions(string Source)"); - await Assert.That(generatedAlias) - .Contains(": this(Source, default!)"); + await Assert.That(exception.Message).Contains("newly required member(s) Destination have no baseline value"); } finally { @@ -1519,7 +1539,7 @@ await Assert.That(generatedAlias) } [Test] - public async Task ApiCompatibilityPreserver_Retains_Parameterless_Command_Group_Alias_Constructor() + public async Task ApiCompatibilityPreserver_Rejects_Parameterless_Alias_Constructor_For_New_Required_Members() { var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}"); var optionsDirectory = Path.Combine(root, "src", "ModularPipelines.Tool", "Options"); @@ -1553,14 +1573,10 @@ await File.WriteAllTextAsync( ], }; - var preserved = GeneratedApiCompatibilityPreserver.Preserve(tool, root); - var generatedAlias = (await new OptionsClassGenerator().GenerateAsync(preserved)) - .Single(file => Path.GetFileName(file.RelativePath) - .Equals("ToolBuilderBakeOptions.Generated.cs", StringComparison.Ordinal)) - .Content; + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve(tool, root)); - await Assert.That(generatedAlias).Contains("public ToolBuilderBakeOptions()"); - await Assert.That(generatedAlias).Contains(": this(default!)"); + await Assert.That(exception.Message).Contains("newly required member(s) Source have no baseline value"); } finally { @@ -1593,11 +1609,30 @@ await File.WriteAllTextAsync( await File.WriteAllTextAsync( Path.Combine(enumsDirectory, "ToolBuilderBakeProgress.Generated.cs"), "public enum ToolBuilderBakeProgress { [EnumValue(\"plain\")] Plain = 4 }"); + var enumDefinition = new CliEnumDefinition + { + EnumName = "ToolBuildxBakeProgress", + Values = + [ + new CliEnumValue { MemberName = "Plain", CliValue = "plain" }, + new CliEnumValue { MemberName = "Tty", CliValue = "tty" }, + ], + }; var command = Command( "ToolBuildxBakeOptions", "ToolOptions", ["buildx", "bake"], - subDomainGroup: "Buildx"); + subDomainGroup: "Buildx", + options: + [ + new CliOptionDefinition + { + SwitchName = "--progress", + PropertyName = "Progress", + CSharpType = "ToolBuildxBakeProgress?", + EnumDefinition = enumDefinition, + }, + ]); var tool = Tool(command) with { CommandGroupAliases = @@ -1631,6 +1666,9 @@ await Assert.That(generatedEnums.Select(file => Path.GetFileName(file.RelativePa .Contains("ToolBuildxBakeProgress.Generated.cs"); await Assert.That(generatedEnums.Select(file => Path.GetFileName(file.RelativePath))) .Contains("ToolBuilderBakeProgress.Generated.cs"); + await Assert.That(generatedEnums.Single(file => Path.GetFileName(file.RelativePath) + .Equals("ToolBuilderBakeProgress.Generated.cs", StringComparison.Ordinal)).Content) + .Contains("Tty"); } } finally @@ -1639,6 +1677,84 @@ await Assert.That(generatedEnums.Select(file => Path.GetFileName(file.RelativePa } } + [Test] + public async Task ApiCompatibilityPreserver_Scopes_Enum_Baselines_To_The_Current_Tool() + { + var root = Path.Combine(Path.GetTempPath(), $"options-api-{Guid.NewGuid():N}"); + var packageDirectory = Path.Combine(root, "src", "ModularPipelines.Tool"); + var optionsDirectory = Path.Combine(packageDirectory, "Options"); + var enumsDirectory = Path.Combine(packageDirectory, "Enums"); + Directory.CreateDirectory(optionsDirectory); + Directory.CreateDirectory(enumsDirectory); + try + { + await File.WriteAllTextAsync( + Path.Combine(optionsDirectory, "ToolRunOptions.Generated.cs"), + "public record ToolRunOptions;"); + await File.WriteAllTextAsync( + Path.Combine(enumsDirectory, "ToolLegacy.Generated.cs"), + "public enum ToolLegacy { Value }"); + await File.WriteAllTextAsync( + Path.Combine(enumsDirectory, "OtherLegacy.Generated.cs"), + "public enum OtherLegacy { Value }"); + + var preserved = GeneratedApiCompatibilityPreserver.Preserve( + Tool(Command("ToolRunOptions", "ToolOptions", ["run"])), + root); + + await Assert.That(preserved.CompatibilityEnums.Select(static definition => definition.EnumName)) + .IsEquivalentTo(["ToolLegacy"]); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + + [Test] + public async Task OptionsClassGenerator_Rejects_Alias_Enum_Nullability_Changes() + { + const string aliasClassName = "ToolBuilderBakeOptions"; + var command = Command( + "ToolBuildxBakeOptions", + "ToolOptions", + ["buildx", "bake"], + subDomainGroup: "Buildx") with + { + AliasCompatibilityProperties = new Dictionary> + { + [aliasClassName] = + [ + new CliAliasCompatibilityProperty + { + PropertyName = "Progress", + AliasCSharpType = "ToolBuilderBakeProgress?", + CanonicalCSharpType = "ToolBuildxBakeProgress", + ObsoleteMessage = "Use the canonical property instead.", + }, + ], + }, + }; + var tool = Tool(command) with + { + CommandGroupAliases = + [ + new CliCommandGroupAlias + { + Alias = "builder", + CanonicalCommand = "buildx", + ObsoleteMessage = "Use buildx instead.", + }, + ], + }; + + var exception = Assert.Throws(() => + new OptionsClassGenerator().GenerateAsync(tool)); + + await Assert.That(exception.Message) + .Contains("alias property Progress because its nullability changed"); + } + [Test] public async Task ApiCompatibilityPreserver_Rekeys_Documentation_Examples_After_Required_Rename() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs index d950762a9b6..421cb8d9331 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Scrapers/DockerCliCompatibilityTests.cs @@ -66,6 +66,24 @@ public async Task Switch_Normalization_Rejects_Distinct_Options_With_One_Canonic .And.HasMessageContaining("maps both '--current' and '--legacy'"); } + [Test] + public async Task Switch_Normalization_Treats_Source_Switch_Casing_As_Distinct() + { + const string helpText = """ + Usage: fake run [OPTIONS] + + Options: + --current string Current value + --CURRENT string Upper-case value + """; + + await Assert.That(() => new CollidingSwitchScraper().Parse( + ["fake", "run"], + helpText)) + .Throws() + .And.HasMessageContaining("maps both '--current' and '--CURRENT'"); + } + private static CliCommandDefinition CreateCommand(string commandGroup) { return new CliCommandDefinition diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index 9f68ecefd9a..238569a2c09 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -27,9 +27,12 @@ public static CliToolDefinition Preserve(CliToolDefinition tool, string outputDi } var enumBaseline = ReadEnumBaseline(Path.Combine( - outputDirectory, - tool.OutputDirectory, - "Enums")); + outputDirectory, + tool.OutputDirectory, + "Enums")) + .Where(pair => pair.Key.StartsWith(tool.NamespacePrefix, StringComparison.Ordinal)) + .ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal); + MergeCurrentAliasEnumValues(tool, enumBaseline); tool = tool with { CompatibilityEnums = tool.CompatibilityEnums @@ -321,13 +324,6 @@ private static CliCommandDefinition Preserve( violations); } - PreserveCompatibilityConstructors( - baselineProperties, - baselineConstructors, - positionalArguments, - options, - compatibilityConstructors); - if (violations.Count > 0) { throw new InvalidOperationException( @@ -336,6 +332,13 @@ private static CliCommandDefinition Preserve( + string.Join(Environment.NewLine, violations.Select(violation => $"- {violation}"))); } + PreserveCompatibilityConstructors( + baselineProperties, + baselineConstructors, + positionalArguments, + options, + compatibilityConstructors); + return command with { Options = options, @@ -432,6 +435,7 @@ private static void PreserveCompatibilityProperty( PropertyName = baseline.PropertyName, CSharpType = baseline.CSharpType, ForwardToPropertyName = baseline.ForwardToPropertyName, + UseInitAccessor = baseline.UseInitAccessor, ObsoleteMessage = baseline.ObsoleteMessage ?? $"{baseline.PropertyName} is retained for compatibility.", }); @@ -618,6 +622,7 @@ private static void AddRenamedCurrentProperty( PropertyName = propertyName, CSharpType = cSharpType, ForwardToPropertyName = forwardToPropertyName, + UseInitAccessor = true, ObsoleteMessage = $"Use {forwardToPropertyName} instead.", }); @@ -701,14 +706,27 @@ private static void PreserveCompatibilityConstructors( return; } + var baselineRequired = baselineProperties + .Where(static property => property.IsRequired && !property.IsCompatibility) + .ToArray(); + var addedRequired = currentRequired + .Where(current => !baselineRequired.Any(baseline => + baseline.PropertyName.Equals(current.PropertyName, StringComparison.Ordinal) + && baseline.CSharpType.Equals(current.CSharpType, StringComparison.Ordinal))) + .Select(static property => property.PropertyName) + .ToArray(); + if (currentRequired.Count > baselineRequired.Length && addedRequired.Length > 0) + { + throw new InvalidOperationException( + "Cannot retain generated constructors because newly required member(s) " + + $"{string.Join(", ", addedRequired)} have no baseline value."); + } + foreach (var constructor in baselineConstructors) { AddCompatibilityConstructor(compatibilityConstructors, constructor, currentRequired); } - var baselineRequired = baselineProperties - .Where(static property => property.IsRequired && !property.IsCompatibility) - .ToArray(); if (HasSameConstructorContract(baselineRequired, currentRequired)) { return; @@ -790,10 +808,13 @@ private static bool HasSameConstructorSignature( return false; } - return left.Select(GetConstructorParameterType) - .SequenceEqual(right.Select(GetConstructorParameterType), StringComparer.Ordinal); + return left.Select(GetConstructorParameterSignatureType) + .SequenceEqual(right.Select(GetConstructorParameterSignatureType), StringComparer.Ordinal); } + private static string GetConstructorParameterSignatureType(T parameter) => + GetConstructorParameterType(parameter).TrimEnd('?'); + private static string GetConstructorParameterType(T parameter) => parameter switch { GeneratedApiProperty property => property.CSharpType, @@ -936,6 +957,50 @@ private static Dictionary ReadEnumBaseline(string enu return baseline; } + private static void MergeCurrentAliasEnumValues( + CliToolDefinition tool, + IDictionary enumBaseline) + { + var currentAliasEnums = tool.CommandGroupAliases + .SelectMany(alias => tool.Commands + .Where(command => command.CommandParts.Length > 0 + && command.CommandParts[0].Equals( + alias.CanonicalCommand, + StringComparison.OrdinalIgnoreCase)) + .SelectMany(command => command.Options + .Where(static option => option.EnumDefinition is not null) + .Select(option => option.EnumDefinition! with + { + EnumName = GeneratorUtils.GetAliasedClassName( + tool, + alias, + option.EnumDefinition!.EnumName), + }))) + .GroupBy(static definition => definition.EnumName, StringComparer.Ordinal) + .ToDictionary( + static group => group.Key, + static group => group.SelectMany(static definition => definition.Values) + .DistinctBy(static value => value.CliValue, StringComparer.Ordinal) + .ToArray(), + StringComparer.Ordinal); + + foreach (var pair in enumBaseline.ToArray()) + { + if (!currentAliasEnums.TryGetValue(pair.Key, out var currentValues)) + { + continue; + } + + enumBaseline[pair.Key] = pair.Value with + { + Values = pair.Value.Values + .Concat(currentValues) + .DistinctBy(static value => value.CliValue, StringComparer.Ordinal) + .ToArray(), + }; + } + } + private static int? GetEnumNumericValue(ExpressionSyntax? expression) => expression switch { LiteralExpressionSyntax literal when literal.Token.Value is int value => value, @@ -1157,7 +1222,9 @@ private static GeneratedApiProperty ReadProperty( isRequired, obsolete is not null, GetForwardTarget(accessorList), - GetStringArgument(obsolete)); + GetStringArgument(obsolete), + accessorList?.Accessors.Any(static accessor => + accessor.IsKind(SyntaxKind.InitAccessorDeclaration)) == true); } private static AttributeSyntax? FindAttribute( @@ -1196,7 +1263,8 @@ internal sealed record GeneratedApiProperty( bool IsRequired, bool IsCompatibility, string? ForwardToPropertyName, - string? ObsoleteMessage); + string? ObsoleteMessage, + bool UseInitAccessor = false); internal sealed record GeneratedApiBaseline( IReadOnlyList Properties, diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs index ac3d14f9ebe..bbce36767c3 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs @@ -46,7 +46,8 @@ internal static void GenerateCompatibilityProperty( sb.AppendLine($" public {modifiers}{property.CSharpType} {property.PropertyName}"); sb.AppendLine(" {"); sb.AppendLine($" get => {property.ForwardToPropertyName};"); - sb.AppendLine($" set => {property.ForwardToPropertyName} = value;"); + var setter = property.UseInitAccessor ? "init" : "set"; + sb.AppendLine($" {setter} => {property.ForwardToPropertyName} = value;"); sb.AppendLine(" }"); } diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs index e1b52c9b457..9aa19e53213 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs @@ -259,13 +259,21 @@ private static void GenerateAliasCompatibilityProperty( $"Cannot retain alias property {property.PropertyName} because its collection shape changed."); } + var aliasIsNullable = property.AliasCSharpType.EndsWith('?'); + var canonicalIsNullable = property.CanonicalCSharpType.EndsWith('?'); + if (aliasIsNullable != canonicalIsNullable) + { + throw new InvalidOperationException( + $"Cannot retain alias property {property.PropertyName} because its nullability changed."); + } + sb.AppendLine($" [Obsolete({GeneratorUtils.FormatStringLiteral(property.ObsoleteMessage)})]"); sb.AppendLine($" public new {property.AliasCSharpType} {property.PropertyName}"); sb.AppendLine(" {"); if (aliasIsEnumerable) { - var baseNullableOperator = property.CanonicalCSharpType.EndsWith('?') ? "?" : string.Empty; - var aliasNullableOperator = property.AliasCSharpType.EndsWith('?') ? "?" : string.Empty; + var baseNullableOperator = canonicalIsNullable ? "?" : string.Empty; + var aliasNullableOperator = aliasIsNullable ? "?" : string.Empty; sb.AppendLine( $" get => base.{property.PropertyName}{baseNullableOperator}.Select(" + $"static value => ({aliasEnumName})(int)value);"); @@ -273,8 +281,7 @@ private static void GenerateAliasCompatibilityProperty( $" set => base.{property.PropertyName} = value{aliasNullableOperator}.Select(" + $"static value => ({canonicalEnumName})(int)value);"); } - else if (property.AliasCSharpType.EndsWith('?') - && property.CanonicalCSharpType.EndsWith('?')) + else if (aliasIsNullable) { sb.AppendLine($" get => base.{property.PropertyName} is null"); sb.AppendLine(" ? null"); diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs index 984e7ebf029..549d236980f 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Models/CliCommandDefinition.cs @@ -93,7 +93,7 @@ public record CliCommandDefinition public IReadOnlyList CompatibilityProperties { get; init; } = []; /// - /// Secondary constructors retained when the CLI adds required members to the primary constructor. + /// Previously generated secondary constructors retained for source compatibility. /// public IReadOnlyList CompatibilityConstructors { get; init; } = []; @@ -212,6 +212,11 @@ public record CliCompatibilityProperty /// public string? ForwardToPropertyName { get; init; } + /// + /// Whether the forwarding accessor must use init because its target is init-only. + /// + public bool UseInitAccessor { get; init; } + /// /// Obsolete diagnostic shown to consumers. /// diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs index 96f2802fc36..65e5301f1bf 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Scrapers/Cli/CobraCliScraper.cs @@ -320,7 +320,7 @@ private List ParseOptions(string helpText, string[] command { if (!existingScrapedLongForm.Equals( scrapedLongForm, - StringComparison.OrdinalIgnoreCase)) + StringComparison.Ordinal)) { throw new InvalidOperationException( $"Option switch normalization for '{string.Join(' ', commandParts)}' " From 6ea99c5b47eec8dc1afdd0e5136c28ba256c53a1 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 17 Aug 2026 02:39:27 +0100 Subject: [PATCH 19/21] fix: validate reused compatibility option names --- .../Generators/GeneratorHardeningTests.cs | 55 ++++++++++++++++++- .../GeneratedApiCompatibilityPreserver.cs | 12 ++-- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs index 184baacfa93..91973229d8c 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs @@ -105,14 +105,15 @@ private static GeneratedApiProperty BaselineProperty( string cSharpType, string? switchName = null, int? argumentPosition = null, - bool isRequired = false) => + bool isRequired = false, + bool isCompatibility = false) => new( propertyName, cSharpType, switchName, argumentPosition, isRequired, - false, + isCompatibility, null, null); @@ -942,6 +943,56 @@ await Assert.That(exception.Message) .Contains("ToolCopyOptions.CommandOptions changed CLI switch or argument position"); } + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Reused_Compatibility_Property_With_Different_Type() + { + var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with + { + Options = + [ + new CliOptionDefinition + { + SwitchName = "--restored", + PropertyName = "RemovedFlag", + CSharpType = "string?", + }, + ], + }; + + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("RemovedFlag", "bool?", isCompatibility: true)])); + + await Assert.That(exception.Message) + .Contains("ToolCopyOptions.RemovedFlag changed type from bool? to string?"); + } + + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Reused_Compatibility_Property_With_Cli_Identity() + { + var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with + { + Options = + [ + new CliOptionDefinition + { + SwitchName = "--restored", + PropertyName = "RemovedFlag", + CSharpType = "bool?", + }, + ], + }; + + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("RemovedFlag", "bool?", isCompatibility: true)])); + + await Assert.That(exception.Message) + .Contains("ToolCopyOptions.RemovedFlag changed CLI switch or argument position"); + } + [Test] public async Task ApiCompatibilityPreserver_Uses_The_Emitted_Optional_Value_Type() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index 238569a2c09..09436e42e3f 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -358,12 +358,6 @@ private static void PreserveBaselineProperty( ICollection compatibilityProperties, List violations) { - if (baseline.IsCompatibility) - { - PreserveCompatibilityProperty(baseline, compatibilityProperties); - return; - } - var sameName = currentProperties.FirstOrDefault(property => property.PropertyName.Equals(baseline.PropertyName, StringComparison.Ordinal)); if (sameName is not null) @@ -372,6 +366,12 @@ private static void PreserveBaselineProperty( return; } + if (baseline.IsCompatibility) + { + PreserveCompatibilityProperty(baseline, compatibilityProperties); + return; + } + var replacement = currentProperties.FirstOrDefault(property => HasSameCliIdentity(property, baseline)); if (TryRecordRemovedPropertyViolation(command, baseline, replacement, violations)) From c8fe2407303222f179badace83438eefcccab79b Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 17 Aug 2026 06:57:39 +0100 Subject: [PATCH 20/21] docs: regenerate Kustomize reference (#3935) --- docs/docs/mp-packages/cli/kustomize.md | 30 +++++++++++--------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/docs/docs/mp-packages/cli/kustomize.md b/docs/docs/mp-packages/cli/kustomize.md index 5ab193ef75e..30bae4e5bf9 100644 --- a/docs/docs/mp-packages/cli/kustomize.md +++ b/docs/docs/mp-packages/cli/kustomize.md @@ -7,7 +7,13 @@ title: kustomize CLI reference `ModularPipelines.Kubernetes` provides strongly typed access to the `kustomize` CLI. -## Installation +## Executable prerequisite + +This package does not install the `kustomize` executable. Install it separately and ensure `kustomize` is available on `PATH`. + +Follow the executable's official documentation for installation instructions. + +## Package installation ```shell dotnet add package ModularPipelines.Kubernetes @@ -17,23 +23,10 @@ Resolve the service with `context.Tools.Kustomize`. For projects older than C# 1 ## Module example -```csharp -using ModularPipelines.Context; -using ModularPipelines.Models; -using ModularPipelines.Modules; -using ModularPipelines.Kubernetes.Options; +Resolve the service in a module, then select a command from the table below. A runnable example is omitted when no command has complete safety metadata: -public class RunCommandModule : Module -{ - protected override async Task ExecuteAsync( - IModuleContext context, - CancellationToken cancellationToken) - { - return await context.Tools.Kustomize.BuildAsync( - new KustomizeBuildOptions(), - cancellationToken: cancellationToken); - } -} +```csharp +var kustomize = context.Tools.Kustomize; ``` ## Commands @@ -41,11 +34,13 @@ public class RunCommandModule : Module | CLI command | Options record | | --- | --- | | `kustomize build` | `KustomizeBuildOptions` | +| `kustomize cfg` | `KustomizeCfgOptions` | | `kustomize cfg cat` | `KustomizeCfgCatOptions` | | `kustomize cfg count` | `KustomizeCfgCountOptions` | | `kustomize cfg grep` | `KustomizeCfgGrepOptions` | | `kustomize cfg tree` | `KustomizeCfgTreeOptions` | | `kustomize create` | `KustomizeCreateOptions` | +| `kustomize edit` | `KustomizeEditOptions` | | `kustomize edit add` | `KustomizeEditAddOptions` | | `kustomize edit add annotation` | `KustomizeEditAddAnnotationOptions` | | `kustomize edit add base` | `KustomizeEditAddBaseOptions` | @@ -80,5 +75,6 @@ public class RunCommandModule : Module | `kustomize edit set namesuffix` | `KustomizeEditSetNamesuffixOptions` | | `kustomize edit set replicas` | `KustomizeEditSetReplicasOptions` | | `kustomize edit set secret` | `KustomizeEditSetSecretOptions` | +| `kustomize fn` | `KustomizeFnOptions` | | `kustomize fn run` | `KustomizeFnRunOptions` | | `kustomize localize` | `KustomizeLocalizeOptions` | From 866a47287afa6c3c9bafb78ec7b27e660e48d8f1 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:47:51 +0100 Subject: [PATCH 21/21] fix: validate supplied compatibility aliases (#3935) --- .../Generators/GeneratorHardeningTests.cs | 64 ++++++++++++++++++- .../GeneratedApiCompatibilityPreserver.cs | 62 ++++++++++++++---- 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs index 91973229d8c..a343699c46c 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator.Tests/Generators/GeneratorHardeningTests.cs @@ -106,7 +106,9 @@ private static GeneratedApiProperty BaselineProperty( string? switchName = null, int? argumentPosition = null, bool isRequired = false, - bool isCompatibility = false) => + bool isCompatibility = false, + string? forwardToPropertyName = null, + bool useInitAccessor = false) => new( propertyName, cSharpType, @@ -114,8 +116,9 @@ private static GeneratedApiProperty BaselineProperty( argumentPosition, isRequired, isCompatibility, + forwardToPropertyName, null, - null); + useInitAccessor); private static CliOptionDefinition RequiredOption(string switchName, string propertyName) => new() @@ -993,6 +996,63 @@ await Assert.That(exception.Message) .Contains("ToolCopyOptions.RemovedFlag changed CLI switch or argument position"); } + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Conflicting_Supplied_Compatibility_Property_Type() + { + var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with + { + CompatibilityProperties = + [ + new CliCompatibilityProperty + { + PropertyName = "RemovedFlag", + CSharpType = "string?", + ObsoleteMessage = "Still retained.", + }, + ], + }; + + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve( + command, + [BaselineProperty("RemovedFlag", "bool?", isCompatibility: true)])); + + await Assert.That(exception.Message) + .Contains("ToolCopyOptions.RemovedFlag compatibility property changed type from bool? to string?"); + } + + [Test] + public async Task ApiCompatibilityPreserver_Rejects_Conflicting_Supplied_Compatibility_Forwarding() + { + var command = Command("ToolCopyOptions", "ToolOptions", ["copy"]) with + { + CompatibilityProperties = + [ + new CliCompatibilityProperty + { + PropertyName = "OldName", + CSharpType = "string?", + ForwardToPropertyName = "DifferentName", + ObsoleteMessage = "Use DifferentName.", + }, + ], + }; + + var exception = Assert.Throws(() => + GeneratedApiCompatibilityPreserver.Preserve( + command, + [ + BaselineProperty( + "OldName", + "string?", + isCompatibility: true, + forwardToPropertyName: "CurrentName"), + ])); + + await Assert.That(exception.Message) + .Contains("ToolCopyOptions.OldName compatibility property changed forwarding target from CurrentName to DifferentName"); + } + [Test] public async Task ApiCompatibilityPreserver_Uses_The_Emitted_Optional_Value_Type() { diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs index 09436e42e3f..c000e29a21a 100644 --- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs +++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratedApiCompatibilityPreserver.cs @@ -368,7 +368,11 @@ private static void PreserveBaselineProperty( if (baseline.IsCompatibility) { - PreserveCompatibilityProperty(baseline, compatibilityProperties); + PreserveCompatibilityProperty( + command, + baseline, + compatibilityProperties, + violations); return; } @@ -426,19 +430,51 @@ private static bool TryRecordRemovedPropertyViolation( } private static void PreserveCompatibilityProperty( + CliCommandDefinition command, GeneratedApiProperty baseline, - ICollection compatibilityProperties) => - AddCompatibilityProperty( - compatibilityProperties, - new CliCompatibilityProperty - { - PropertyName = baseline.PropertyName, - CSharpType = baseline.CSharpType, - ForwardToPropertyName = baseline.ForwardToPropertyName, - UseInitAccessor = baseline.UseInitAccessor, - ObsoleteMessage = baseline.ObsoleteMessage - ?? $"{baseline.PropertyName} is retained for compatibility.", - }); + ICollection compatibilityProperties, + ICollection violations) + { + var supplied = compatibilityProperties.FirstOrDefault(property => + property.PropertyName.Equals(baseline.PropertyName, StringComparison.Ordinal)); + if (supplied is null) + { + AddCompatibilityProperty( + compatibilityProperties, + new CliCompatibilityProperty + { + PropertyName = baseline.PropertyName, + CSharpType = baseline.CSharpType, + ForwardToPropertyName = baseline.ForwardToPropertyName, + UseInitAccessor = baseline.UseInitAccessor, + ObsoleteMessage = baseline.ObsoleteMessage + ?? $"{baseline.PropertyName} is retained for compatibility.", + }); + return; + } + + if (!supplied.CSharpType.Equals(baseline.CSharpType, StringComparison.Ordinal)) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} compatibility property changed type from " + + $"{baseline.CSharpType} to {supplied.CSharpType}"); + } + else if (!string.Equals( + supplied.ForwardToPropertyName, + baseline.ForwardToPropertyName, + StringComparison.Ordinal)) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} compatibility property changed forwarding target from " + + $"{baseline.ForwardToPropertyName ?? ""} to {supplied.ForwardToPropertyName ?? ""}"); + } + else if (supplied.UseInitAccessor != baseline.UseInitAccessor) + { + violations.Add( + $"{command.ClassName}.{baseline.PropertyName} compatibility property changed accessor from " + + $"{(baseline.UseInitAccessor ? "init" : "set")} to {(supplied.UseInitAccessor ? "init" : "set")}"); + } + } private static void ValidateMatchingProperty( CliCommandDefinition command,