Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
62d31da
docs: Add design for ModuleResult discriminated union pattern
thomhurst Jan 7, 2026
b663d02
docs: add implementation plan for ModuleResult discriminated union
thomhurst Jan 7, 2026
78e6a4b
refactor(IModuleResult): add safe accessor properties for discriminat…
thomhurst Jan 7, 2026
bba491f
refactor(ModuleResult): implement discriminated union with Success/Fa…
thomhurst Jan 7, 2026
55739b3
refactor: remove SkippedModuleResult and TimedOutModuleResult (replac…
thomhurst Jan 7, 2026
c8fd00a
refactor(ModuleResultFactory): simplify to use ModuleResult static fa…
thomhurst Jan 7, 2026
bfe8050
refactor(ModuleExecutionPipeline): use discriminated union factory me…
thomhurst Jan 7, 2026
a977019
deprecate: mark ModuleFailedException and ModuleSkippedException as o…
thomhurst Jan 7, 2026
c994a53
test: update test helpers for discriminated union pattern
thomhurst Jan 7, 2026
9264354
test: update unit tests for discriminated union pattern
thomhurst Jan 7, 2026
2dac9b2
fix: update Build project modules for discriminated union pattern
thomhurst Jan 7, 2026
ae1597a
fix: add JSON serialization support and fix history repository handling
thomhurst Jan 7, 2026
fa184c0
fix: address code review issues for discriminated union PR
thomhurst Jan 7, 2026
21bb7c3
fix: address PR #1895 review comments
thomhurst Jan 7, 2026
0c84019
Merge main into feature/1869-module-result-discriminated-union
thomhurst Jan 7, 2026
00e57f7
fix: update DirectModuleHooksTests to use new discriminated union API
thomhurst Jan 7, 2026
2245536
fix: update Azure example modules to use ValueOrDefault
thomhurst Jan 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions docs/plans/2026-01-07-module-result-discriminated-union.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ switch (result)
case ModuleResult<string>.Failure { Exception: var ex }:
Console.WriteLine($"Failed: {ex.Message}");
break;
case ModuleResult<string>.Skipped { Decision.Reason: var reason }:
Console.WriteLine($"Skipped: {reason}");
case ModuleResult<string>.Skipped { Decision: var skip }:
Console.WriteLine($"Skipped: {skip.Reason}");
break;
}
```
Expand Down Expand Up @@ -172,11 +172,13 @@ internal static Skipped CreateSkipped(SkipDecision decision, ModuleExecutionCont

### Removed
- `ModuleResult<T>.Value` property (the throwing one)
- `ModuleFailedException` class
- `ModuleSkippedException` class
- `SkippedModuleResult<T>` subclass
- `TimedOutModuleResult<T>` subclass
Comment on lines +173 to +176
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation inaccuracy. The breaking changes section states that ModuleFailedException and ModuleSkippedException classes are "Removed", but they are actually deprecated with [Obsolete] attributes, not deleted. Update the documentation to reflect that these classes are deprecated rather than removed to provide a more accurate migration path.

Copilot uses AI. Check for mistakes.

### Deprecated (not removed)
- `ModuleFailedException` class (marked with [Obsolete])
- `ModuleSkippedException` class (marked with [Obsolete])

### Preserved
- `ModuleResultType` property (now computed)
- `IModuleResult` interface
Expand Down
4 changes: 2 additions & 2 deletions src/ModularPipelines.Build/Modules/CreateReleaseModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ public Task<SkipDecision> ShouldSkip(IPipelineContext context)
}

return await context.GitHub().Client.Repository.Release.Create(repositoryId,
new NewRelease($"v{versionInfoResult.Value}")
new NewRelease($"v{versionInfoResult.ValueOrDefault}")
{
Name = versionInfoResult.Value,
Name = versionInfoResult.ValueOrDefault,
GenerateReleaseNotes = true,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class FindProjectDependenciesModule : Module<FindProjectDependenciesModul

var dependencies = new List<File>();

foreach (var file in projects.Value!)
foreach (var file in projects.ValueOrDefault!)
{
var projectRootElement = ProjectRootElement.Open(file)!;

Expand All @@ -27,7 +27,7 @@ public class FindProjectDependenciesModule : Module<FindProjectDependenciesModul
foreach (var reference in projectReferences)
{
var name = Path.GetFileName(reference);
var project = projects.Value!.FirstOrDefault(x => x.Name == name);
var project = projects.ValueOrDefault!.FirstOrDefault(x => x.Name == name);

if (project != null)
{
Expand All @@ -36,7 +36,7 @@ public class FindProjectDependenciesModule : Module<FindProjectDependenciesModul
}
}

var projectDependencies = new ProjectDependencies(Dependencies: dependencies.Distinct().ToList(), Others: projects.Value!.Except(dependencies).Distinct().ToList());
var projectDependencies = new ProjectDependencies(Dependencies: dependencies.Distinct().ToList(), Others: projects.ValueOrDefault!.Except(dependencies).Distinct().ToList());

LogProjects(context, projectDependencies);

Expand Down
2 changes: 1 addition & 1 deletion src/ModularPipelines.Build/Modules/GenerateReadMeModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class GenerateReadMeModule : Module<IDictionary<string, object>>, IAlways

var projects = context.GetModule<FindProjectsModule, IReadOnlyList<File>>();

foreach (var project in projects.Value!
foreach (var project in projects.ValueOrDefault!
.Where(x => !x.NameWithoutExtension.StartsWith("ModularPipelines.Analyzers")))
{
var moduleName = project.NameWithoutExtension;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public Task<bool> ShouldIgnoreFailures(IPipelineContext context, Exception excep
return await context.DotNet().Nuget.Add.Source(new DotNetNugetAddSourceOptions
{
Name = "ModularPipelinesLocalNuGet",
Packagesourcepath = localNugetPathResult.Value.AssertExists(),
Packagesourcepath = localNugetPathResult.ValueOrDefault.AssertExists(),
}, cancellationToken: cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public class UploadPackagesToLocalNuGetModule : Module<CommandResult[]>

return await NugetUploadHelper.UploadPackagesAsync(
context,
packagePaths.Value!,
source: localRepoLocation.Value.AssertExists()!.Path,
packagePaths.ValueOrDefault!,
source: localRepoLocation.ValueOrDefault.AssertExists()!.Path,
apiKey: null,
cancellationToken);
}
Expand Down
12 changes: 6 additions & 6 deletions src/ModularPipelines.Build/Modules/PackProjectsModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,23 @@ public class PackProjectsModule : Module<CommandResult[]>

var changedFiles = context.GetModule<ChangedFilesInPullRequestModule, IReadOnlyList<File>>();

var dependencies = await projectFiles.Value!.Dependencies
var dependencies = await projectFiles.ValueOrDefault!.Dependencies
.ToAsyncProcessorBuilder()
.SelectAsync(async projectFile => await Pack(context, cancellationToken, projectFile, packageVersion))
.ProcessOneAtATime();

var gitVersioningInformation = await context.Git().Versioning.GetGitVersioningInformation();

var others = await projectFiles.Value!.Others
var others = await projectFiles.ValueOrDefault!.Others
.Where(x =>
{
if (changedFiles.SkipDecision.ShouldSkip)
if (changedFiles.SkipDecisionOrDefault?.ShouldSkip == true)
{
return true;
}

return ProjectHasChanged(x,
changedFiles.Value!, context);
changedFiles.ValueOrDefault!, context);
})
.ToAsyncProcessorBuilder()
.SelectAsync(async projectFile => await Pack(context, cancellationToken, projectFile, packageVersion))
Expand Down Expand Up @@ -79,8 +79,8 @@ private static async Task<CommandResult> Pack(IModuleContext context, Cancellati
NoRestore = true,
Properties = new List<KeyValue>
{
("PackageVersion", packageVersion.Value!),
("Version", packageVersion.Value!),
("PackageVersion", packageVersion.ValueOrDefault!),
("Version", packageVersion.ValueOrDefault!),
},
}, cancellationToken: cancellationToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class PackagePathsParserModule : Module<List<File>>
{
var packPackagesModuleResult = context.GetModule<PackProjectsModule, CommandResult[]>();

return Task.FromResult<List<File>?>(packPackagesModuleResult.Value!
return Task.FromResult<List<File>?>(packPackagesModuleResult.ValueOrDefault!
.Select(x => x.StandardOutput)
.Select(x => x.Split(PackageCreationSuccessPrefix)[1])
.Select(x => x.Split(PackagePathSuffix)[0])
Expand Down
4 changes: 2 additions & 2 deletions src/ModularPipelines.Build/Modules/PushVersionTagModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public Task<bool> ShouldIgnoreFailures(IPipelineContext context, Exception excep
{
var versionInformation = ((IModuleContext) context).GetModule<NugetVersionGeneratorModule, string>();

return Task.FromResult(exception.Message.Contains($"tag 'v{versionInformation.Value!}' already exists"));
return Task.FromResult(exception.Message.Contains($"tag 'v{versionInformation.ValueOrDefault!}' already exists"));
}

public override async Task<CommandResult?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
Expand All @@ -27,7 +27,7 @@ public Task<bool> ShouldIgnoreFailures(IPipelineContext context, Exception excep

await context.Git().Commands.Tag(new GitTagOptions
{
Arguments = [$"v{versionInformation.Value!}"],
Arguments = [$"v{versionInformation.ValueOrDefault!}"],
}, token: cancellationToken);

return await context.Git().Commands.Push(new GitPushOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public Task<SkipDecision> ShouldSkip(IPipelineContext context)

return await NugetUploadHelper.UploadPackagesAsync(
context,
packagePaths.Value!,
packagePaths.ValueOrDefault!,
source: "https://api.nuget.org/v3/index.json",
apiKey: _nugetSettings.Value.ApiKey,
cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public class AssignAccessToBlobStorageModule : Module<RoleAssignmentResource>
var storageAccount = context.GetModule<ProvisionBlobStorageAccountModule, StorageAccountResource>();

var roleAssignmentResource = await context.Azure().Provisioner.Security.RoleAssignment(
storageAccount.Value!.Id,
new RoleAssignmentCreateOrUpdateContent(WellKnownRoleDefinitions.BlobStorageOwnerDefinitionId, userAssignedIdentity.Value!.Data.PrincipalId!.Value)
storageAccount.ValueOrDefault!.Id,
new RoleAssignmentCreateOrUpdateContent(WellKnownRoleDefinitions.BlobStorageOwnerDefinitionId, userAssignedIdentity.ValueOrDefault!.Data.PrincipalId!.Value)
);

return roleAssignmentResource.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class ProvisionAzureFunction : Module<WebSiteResource>
{
Identity = new ManagedServiceIdentity(ManagedServiceIdentityType.UserAssigned)
{
UserAssignedIdentities = { { userAssignedIdentity.Value!.Id, new UserAssignedIdentity() } },
UserAssignedIdentities = { { userAssignedIdentity.ValueOrDefault!.Id, new UserAssignedIdentity() } },
},
SiteConfig = new SiteConfigProperties
{
Expand All @@ -40,12 +40,12 @@ public class ProvisionAzureFunction : Module<WebSiteResource>
new()
{
Name = "BlobStorageConnectionString",
Value = storageAccount.Value!.Data.PrimaryEndpoints.BlobUri.AbsoluteUri,
Value = storageAccount.ValueOrDefault!.Data.PrimaryEndpoints.BlobUri.AbsoluteUri,
},
new()
{
Name = "BlobContainerName",
Value = blobContainer.Value!.Data.Name,
Value = blobContainer.ValueOrDefault!.Data.Name,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class ProvisionBlobStorageContainerModule : Module<BlobContainerResource>
var blobStorageAccount = context.GetModule<ProvisionBlobStorageAccountModule, StorageAccountResource>();

var blobContainerProvisionResponse = await context.Azure().Provisioner.Storage.BlobContainer(
blobStorageAccount.Value!.Id,
blobStorageAccount.ValueOrDefault!.Id,
"MyContainer",
new BlobContainerData()
);
Expand Down
4 changes: 2 additions & 2 deletions src/ModularPipelines.GitHub/GitHubMarkdownSummaryGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ private async Task<string> GetException(PipelineSummary pipelineSummary)

var exception = results
.FirstOrDefault(x => x.ModuleStatus == Status.Failed)
?.Exception
?? results.Select(x => x.Exception).FirstOrDefault();
?.ExceptionOrDefault
?? results.Select(x => x.ExceptionOrDefault).FirstOrDefault();

if (exception is null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ public interface IModuleEndEventReceiver
/// <param name="context">The event context providing module information and control flow.</param>
/// <param name="result">The result of the module execution.</param>
/// <returns>A task representing the async operation.</returns>
Task OnModuleEndAsync(IModuleEventContext context, ModuleResult result);
Task OnModuleEndAsync(IModuleEventContext context, IModuleResult result);
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public async Task InvokeStartReceiversAsync(
public async Task InvokeEndReceiversAsync(
IEnumerable<IModuleEndEventReceiver> receivers,
IModuleEventContext context,
ModuleResult result)
IModuleResult result)
{
foreach (var receiver in receivers)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal interface IAttributeEventInvoker

Task InvokeStartReceiversAsync(IEnumerable<IModuleStartEventReceiver> receivers, IModuleEventContext context);

Task InvokeEndReceiversAsync(IEnumerable<IModuleEndEventReceiver> receivers, IModuleEventContext context, ModuleResult result);
Task InvokeEndReceiversAsync(IEnumerable<IModuleEndEventReceiver> receivers, IModuleEventContext context, IModuleResult result);

Task InvokeFailureReceiversAsync(IEnumerable<IModuleFailureEventReceiver> receivers, IModuleEventContext context, Exception exception);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public async Task InvokeEndEventAsync(ModuleLifecycleContext context, Enums.Stat
_metadataRegistry,
context.CancellationToken);

await _attributeEventInvoker.InvokeEndReceiversAsync(receivers, eventContext, (ModuleResult)result).ConfigureAwait(false);
await _attributeEventInvoker.InvokeEndReceiversAsync(receivers, eventContext, result).ConfigureAwait(false);
}

/// <inheritdoc />
Expand Down
Loading
Loading