diff --git a/.idea/.idea.ModularPipelines/.idea/.name b/.idea/.idea.ModularPipelines/.idea/.name
new file mode 100644
index 0000000000..05dc058d7a
--- /dev/null
+++ b/.idea/.idea.ModularPipelines/.idea/.name
@@ -0,0 +1 @@
+ModularPipelines
\ No newline at end of file
diff --git a/.idea/.idea.ModularPipelines/.idea/aws.xml b/.idea/.idea.ModularPipelines/.idea/aws.xml
new file mode 100644
index 0000000000..9b821f8cfd
--- /dev/null
+++ b/.idea/.idea.ModularPipelines/.idea/aws.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.Pipeline.NET/.idea/aws.xml b/.idea/.idea.Pipeline.NET/.idea/aws.xml
new file mode 100644
index 0000000000..9b821f8cfd
--- /dev/null
+++ b/.idea/.idea.Pipeline.NET/.idea/aws.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.Pipeline.NET/.idea/indexLayout.xml b/.idea/.idea.Pipeline.NET/.idea/indexLayout.xml
new file mode 100644
index 0000000000..7b08163ceb
--- /dev/null
+++ b/.idea/.idea.Pipeline.NET/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.Pipeline.NET/.idea/projectSettingsUpdater.xml b/.idea/.idea.Pipeline.NET/.idea/projectSettingsUpdater.xml
new file mode 100644
index 0000000000..4bb9f4d2a0
--- /dev/null
+++ b/.idea/.idea.Pipeline.NET/.idea/projectSettingsUpdater.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.Pipeline.NET/.idea/workspace.xml b/.idea/.idea.Pipeline.NET/.idea/workspace.xml
new file mode 100644
index 0000000000..8920cea82e
--- /dev/null
+++ b/.idea/.idea.Pipeline.NET/.idea/workspace.xml
@@ -0,0 +1,143 @@
+
+
+
+ Pipeline.NET.Build/Pipeline.NET.Build.csproj
+ Pipeline.NET.Examples/Pipeline.NET.Examples.csproj
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "keyToString": {
+ "RunOnceActivity.OpenProjectViewOnStart": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "WebServerToolWindowFactoryState": "false",
+ "git-widget-placeholder": "feature/initial-work",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "settings.editor.selected.configurable": "editor.preferences.jsOptions",
+ "vue.rearranger.settings.migration": "true"
+ },
+ "keyToStringList": {
+ "rider.external.source.directories": [
+ "C:\\Users\\tom.longhurst\\AppData\\Roaming\\JetBrains\\Rider2023.1\\resharper-host\\DecompilerCache",
+ "C:\\Users\\tom.longhurst\\AppData\\Roaming\\JetBrains\\Rider2023.1\\resharper-host\\SourcesCache",
+ "C:\\Users\\tom.longhurst\\AppData\\Local\\Symbols\\src"
+ ]
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1684830433835
+
+
+ 1684830433835
+
+
+
+
+
+
+ 1685277157807
+
+
+
+ 1685277157807
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ file://$PROJECT_DIR$/Pipeline.NET/Host/PipelineHostBuilder.cs
+ 100
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000000..21eee9b5e3
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+ enable
+ enable
+ preview
+
+
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.Designer.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.Designer.cs
new file mode 100644
index 0000000000..837c320c8e
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.Designer.cs
@@ -0,0 +1,71 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace ModularPipelines.Analyzers {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class CodeFixResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal CodeFixResources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ModularPipelines.Analyzers.CodeFixResources", typeof(CodeFixResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Add DependsOn Attribute.
+ ///
+ internal static string CodeFixTitle {
+ get {
+ return ResourceManager.GetString("CodeFixTitle", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.resx b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.resx
new file mode 100644
index 0000000000..418073e247
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/CodeFixResources.resx
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Add DependsOn Attribute
+ The title of the code fix.
+
+
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/Extensions/SyntaxNodeExtensions.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/Extensions/SyntaxNodeExtensions.cs
new file mode 100644
index 0000000000..59a53dce4a
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/Extensions/SyntaxNodeExtensions.cs
@@ -0,0 +1,23 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace ModularPipelines.Analyzers.Extensions;
+
+internal static class SyntaxNodeExtensions
+{
+ public static SyntaxNode AddUsings(this SyntaxNode documentRoot)
+ {
+ var compilationUnitSyntax = (CompilationUnitSyntax) documentRoot;
+
+ if(compilationUnitSyntax.Usings.Any(x => x.Name.ToFullString() == "ModularPipelines.Attributes"))
+ {
+ return documentRoot;
+ }
+
+ compilationUnitSyntax = compilationUnitSyntax.AddUsings(
+ SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("ModularPipelines.Attributes")));
+
+ return compilationUnitSyntax;
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/MissingDependsOnAttributeCodeFixProvider.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/MissingDependsOnAttributeCodeFixProvider.cs
new file mode 100644
index 0000000000..c02a04e958
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/MissingDependsOnAttributeCodeFixProvider.cs
@@ -0,0 +1,86 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Collections.Immutable;
+using System.Composition;
+using ModularPipelines.Analyzers.Extensions;
+
+namespace ModularPipelines.Analyzers;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MissingDependsOnAttributeCodeFixProvider)), Shared]
+public class MissingDependsOnAttributeCodeFixProvider : CodeFixProvider
+{
+ public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(MissingDependsOnAttributeAnalyzer.DiagnosticId);
+
+ public sealed override FixAllProvider GetFixAllProvider()
+ {
+ // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+
+ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+ var diagnostic = context.Diagnostics.First();
+ var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+ // Find the type declaration identified by the diagnostic.
+ var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType().First();
+
+ // Register a code action that will invoke the fix.
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: CodeFixResources.CodeFixTitle,
+ createChangedDocument: c => AddAttribute(context, declaration, c),
+ equivalenceKey: nameof(CodeFixResources.CodeFixTitle)),
+ diagnostic);
+ }
+
+ private async Task AddAttribute(CodeFixContext context, TypeDeclarationSyntax typeDecl, CancellationToken cancellationToken)
+ {
+ var document = context.Document;
+
+ var syntaxTree = await context.Document.GetSyntaxTreeAsync(cancellationToken);
+
+ var documentRoot = (await document.GetSyntaxRootAsync(cancellationToken))!;
+
+ var name = context.Diagnostics.First().Properties["Name"]!;
+
+ var attributes = typeDecl.AttributeLists.Add(
+ SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(CreateDependsOnAttribute(name, syntaxTree)))
+ .WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed)
+ .NormalizeWhitespace());
+
+
+ return document.WithSyntaxRoot(
+ documentRoot
+ .ReplaceNode(typeDecl, typeDecl.WithAttributeLists(attributes).WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed))
+ .AddUsings()
+ .NormalizeWhitespace()
+ );
+ }
+
+ private static AttributeSyntax CreateDependsOnAttribute(string name, SyntaxTree syntaxTree)
+ {
+ var cSharpParseOptions = (CSharpParseOptions) syntaxTree.Options;
+
+ if (cSharpParseOptions.LanguageVersion.MapSpecifiedToEffectiveVersion() >= (LanguageVersion) 1100)
+ {
+ return SyntaxFactory.Attribute(SyntaxFactory.ParseName($"DependsOn<{name}>"));
+ }
+
+ return SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("DependsOn"))
+ .WithArgumentList(
+ SyntaxFactory.AttributeArgumentList(
+ SyntaxFactory.SingletonSeparatedList(
+ SyntaxFactory.AttributeArgument(
+ SyntaxFactory.TypeOfExpression(SyntaxFactory.ParseTypeName(name))
+ )
+ )
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/ModularPipelines.Analyzers.CodeFixes.csproj b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/ModularPipelines.Analyzers.CodeFixes.csproj
new file mode 100644
index 0000000000..b56698dc81
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.CodeFixes/ModularPipelines.Analyzers.CodeFixes.csproj
@@ -0,0 +1,22 @@
+
+
+
+ netstandard2.0
+ false
+ ModularPipelines.Analyzers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/ModularPipelines.Analyzers.Package.csproj b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/ModularPipelines.Analyzers.Package.csproj
new file mode 100644
index 0000000000..a353236117
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/ModularPipelines.Analyzers.Package.csproj
@@ -0,0 +1,45 @@
+
+
+
+ netstandard2.0
+ false
+ true
+ true
+
+
+
+ ModularPipelines.Analyzers
+ 1.0.0-alpha06
+ Tom Longhurst
+ http://LICENSE_URL_HERE_OR_DELETE_THIS_LINE
+ http://PROJECT_URL_HERE_OR_DELETE_THIS_LINE
+ http://ICON_URL_HERE_OR_DELETE_THIS_LINE
+ http://REPOSITORY_URL_HERE_OR_DELETE_THIS_LINE
+ false
+ ModularPipelines.Analyzers
+ Summary of changes made in this release of the package.
+ Copyright
+ ModularPipelines.Analyzers, analyzers
+ true
+ true
+
+ $(TargetsForTfmSpecificContentInPackage);_AddAnalyzersToOutput
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/tools/install.ps1 b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/tools/install.ps1
new file mode 100644
index 0000000000..f9bd7896a9
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/tools/install.ps1
@@ -0,0 +1,272 @@
+param($installPath, $toolsPath, $package, $project)
+
+if($project.Object.SupportsPackageDependencyResolution)
+{
+ if($project.Object.SupportsPackageDependencyResolution())
+ {
+ # Do not install analyzers via install.ps1, instead let the project system handle it.
+ return
+ }
+}
+
+$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve
+
+foreach($analyzersPath in $analyzersPaths)
+{
+ if (Test-Path $analyzersPath)
+ {
+ # Install the language agnostic analyzers.
+ foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll)
+ {
+ if($project.Object.AnalyzerReferences)
+ {
+ $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
+ }
+ }
+ }
+}
+
+# $project.Type gives the language name like (C# or VB.NET)
+$languageFolder = ""
+if($project.Type -eq "C#")
+{
+ $languageFolder = "cs"
+}
+if($project.Type -eq "VB.NET")
+{
+ $languageFolder = "vb"
+}
+if($languageFolder -eq "")
+{
+ return
+}
+
+foreach($analyzersPath in $analyzersPaths)
+{
+ # Install language specific analyzers.
+ $languageAnalyzersPath = join-path $analyzersPath $languageFolder
+ if (Test-Path $languageAnalyzersPath)
+ {
+ foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll)
+ {
+ if($project.Object.AnalyzerReferences)
+ {
+ $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
+ }
+ }
+ }
+}
+# SIG # Begin signature block
+# MIInugYJKoZIhvcNAQcCoIInqzCCJ6cCAQExDzANBglghkgBZQMEAgEFADB5Bgor
+# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
+# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA/i+qRUHsWzI0s
+# FVk99zLgt/HOEQ33uvkFsWtHTHZgf6CCDYEwggX/MIID56ADAgECAhMzAAACUosz
+# qviV8znbAAAAAAJSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
+# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
+# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
+# bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMjU5WhcNMjIwOTAxMTgzMjU5WjB0MQsw
+# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
+# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
+# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+# AQDQ5M+Ps/X7BNuv5B/0I6uoDwj0NJOo1KrVQqO7ggRXccklyTrWL4xMShjIou2I
+# sbYnF67wXzVAq5Om4oe+LfzSDOzjcb6ms00gBo0OQaqwQ1BijyJ7NvDf80I1fW9O
+# L76Kt0Wpc2zrGhzcHdb7upPrvxvSNNUvxK3sgw7YTt31410vpEp8yfBEl/hd8ZzA
+# v47DCgJ5j1zm295s1RVZHNp6MoiQFVOECm4AwK2l28i+YER1JO4IplTH44uvzX9o
+# RnJHaMvWzZEpozPy4jNO2DDqbcNs4zh7AWMhE1PWFVA+CHI/En5nASvCvLmuR/t8
+# q4bc8XR8QIZJQSp+2U6m2ldNAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
+# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUNZJaEUGL2Guwt7ZOAu4efEYXedEw
+# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
+# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDY3NTk3MB8GA1UdIwQYMBaAFEhu
+# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
+# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
+# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
+# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
+# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFkk3
+# uSxkTEBh1NtAl7BivIEsAWdgX1qZ+EdZMYbQKasY6IhSLXRMxF1B3OKdR9K/kccp
+# kvNcGl8D7YyYS4mhCUMBR+VLrg3f8PUj38A9V5aiY2/Jok7WZFOAmjPRNNGnyeg7
+# l0lTiThFqE+2aOs6+heegqAdelGgNJKRHLWRuhGKuLIw5lkgx9Ky+QvZrn/Ddi8u
+# TIgWKp+MGG8xY6PBvvjgt9jQShlnPrZ3UY8Bvwy6rynhXBaV0V0TTL0gEx7eh/K1
+# o8Miaru6s/7FyqOLeUS4vTHh9TgBL5DtxCYurXbSBVtL1Fj44+Od/6cmC9mmvrti
+# yG709Y3Rd3YdJj2f3GJq7Y7KdWq0QYhatKhBeg4fxjhg0yut2g6aM1mxjNPrE48z
+# 6HWCNGu9gMK5ZudldRw4a45Z06Aoktof0CqOyTErvq0YjoE4Xpa0+87T/PVUXNqf
+# 7Y+qSU7+9LtLQuMYR4w3cSPjuNusvLf9gBnch5RqM7kaDtYWDgLyB42EfsxeMqwK
+# WwA+TVi0HrWRqfSx2olbE56hJcEkMjOSKz3sRuupFCX3UroyYf52L+2iVTrda8XW
+# esPG62Mnn3T8AuLfzeJFuAbfOSERx7IFZO92UPoXE1uEjL5skl1yTZB3MubgOA4F
+# 8KoRNhviFAEST+nG8c8uIsbZeb08SeYQMqjVEmkwggd6MIIFYqADAgECAgphDpDS
+# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
+# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
+# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
+# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
+# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
+# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
+# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
+# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
+# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
+# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
+# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
+# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
+# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
+# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
+# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
+# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
+# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
+# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
+# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
+# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
+# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
+# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
+# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
+# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
+# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
+# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
+# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
+# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
+# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
+# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
+# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
+# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
+# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
+# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
+# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
+# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
+# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
+# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
+# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
+# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIZjzCCGYsCAQEwgZUwfjELMAkG
+# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
+# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
+# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN
+# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
+# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgRjg7DcI6
+# uhYfXWwAQ6hK0mPW7iyr2tzHR0DHSDJkscIwQgYKKwYBBAGCNwIBDDE0MDKgFIAS
+# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
+# BgkqhkiG9w0BAQEFAASCAQB3ERGpqvGnJrsyU0d9lERK2TJW4/OONhZAFjxrEvEk
+# PzdH0Fk0otvagAvjHzJ3q0G8C7gwRbXIyGgiYYIMefheNvgd/UKnubUGEzeG9h0/
+# biX5Ro1mxuHBYvc3vqvWD292jXMg00iRmexDsTny8YgSAAWsTdkE8/W2ooEfbG1T
+# QkCg6ds9btpA1D1znVYpEbviCJoAfHLbNBr5nzAadgWjQM8nnb3UTvmLDIs5b1LO
+# 3lm9w485IBFRnfrj6QinVsCbSD7PU/N1hPY7rKfM9ScZC6QT6kjyuVVa1Ft+VYLH
+# qlV9hE6B4CGeB8qkko4x+MKovgbdpCgYz3eePWCakZywoYIXGTCCFxUGCisGAQQB
+# gjcDAwExghcFMIIXAQYJKoZIhvcNAQcCoIIW8jCCFu4CAQMxDzANBglghkgBZQME
+# AgEFADCCAVkGCyqGSIb3DQEJEAEEoIIBSASCAUQwggFAAgEBBgorBgEEAYRZCgMB
+# MDEwDQYJYIZIAWUDBAIBBQAEIC58WTh4Q8r6c2kVXmD8xoHEhya2jc6YZ43KUAIy
+# flB4AgZh/WKJ50gYEzIwMjIwMjExMTkwMzQwLjE1M1owBIACAfSggdikgdUwgdIx
+# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
+# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1p
+# Y3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhh
+# bGVzIFRTUyBFU046M0JENC00QjgwLTY5QzMxJTAjBgNVBAMTHE1pY3Jvc29mdCBU
+# aW1lLVN0YW1wIFNlcnZpY2WgghFoMIIHFDCCBPygAwIBAgITMwAAAYm0v4YwhBxL
+# jwABAAABiTANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMK
+# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
+# IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0Eg
+# MjAxMDAeFw0yMTEwMjgxOTI3NDFaFw0yMzAxMjYxOTI3NDFaMIHSMQswCQYDVQQG
+# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
+# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQg
+# SXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg
+# RVNOOjNCRDQtNEI4MC02OUMzMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt
+# cCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvQZXxZFm
+# a6plmuOyvNpV8xONOwcYolZG/BjyZWGSk5JOGaLyrKId5VxVHWHlsmJE4Svnzsdp
+# sKmVx8otONveIUFvSceEZp8VXmu5m1fu8L7c+3lwXcibjccqtEvtQslokQVx0r+L
+# 54abrNDarwFG73IaRidIS1i9c+unJ8oYyhDRLrCysFAVxyQhPNZkWK7Z8/VGukaK
+# LAWHXCh/+R53h42gFL+9/mAALxzCXXuofi8f/XKCm7xNwVc1hONCCz6oq94AufzV
+# NkkIW4brUQgYpCcJm9U0XNmQvtropYDn9UtY8YQ0NKenXPtdgLHdQ8Nnv3igErKL
+# rWI0a5n5jjdKfwk+8mvakqdZmlOseeOS1XspQNJAK1uZllAITcnQZOcO5ofjOQ33
+# ujWckAXdz+/x3o7l4AU/TSOMzGZMwhUdtVwC3dSbItpSVFgnjM2COEJ9zgCadvOi
+# rGDLN471jZI2jClkjsJTdgPk343TQA4JFvds/unZq0uLr+niZ3X44OBx2x+gVlln
+# 2c4UbZXNueA4yS1TJGbbJFIILAmTUA9Auj5eISGTbNiyWx79HnCOTar39QEKozm4
+# LnTmDXy0/KI/H/nYZGKuTHfckP28wQS06rD+fDS5xLwcRMCW92DkHXmtbhGyRilB
+# OL5LxZelQfxt54wl4WUC0AdAEolPekODwO8CAwEAAaOCATYwggEyMB0GA1UdDgQW
+# BBSXbx+zR1p4IIAeguA6rHKkrfl7UDAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJl
+# pxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
+# b20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAx
+# MCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3
+# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3Rh
+# bXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoG
+# CCsGAQUFBwMIMA0GCSqGSIb3DQEBCwUAA4ICAQCOtLdpWUI4KwfLLrfaKrLB92Dq
+# bAspGWM41TaO4Jl+sHxPo522uu3GKQCjmkRWreHtlfyy9kOk7LWax3k3ke8Gtfet
+# fbh7qH0LeV2XOWg39BOnHf6mTcZq7FYSZZch1JDQjc98+Odlow+oWih0Dbt4CV/e
+# 19ZcE+1n1zzWkskUEd0f5jPIUis33p+vkY8szduAtCcIcPFUhI8Hb5alPUAPMjGz
+# wKb7NIKbnf8j8cP18As5IveckF0oh1cw63RY/vPK62LDYdpi7WnG2ObvngfWVKtw
+# iwTI4jHj2cO9q37HDe/PPl216gSpUZh0ap24mKmMDfcKp1N4mEdsxz4oseOrPYeF
+# sHHWJFJ6Aivvqn70KTeJpp5r+DxSqbeSy0mxIUOq/lAaUxgNSQVUX26t8r+fciko
+# fKv23WHrtRV3t7rVTsB9YzrRaiikmz68K5HWdt9MqULxPQPo+ppZ0LRqkOae466+
+# UKRY0JxWtdrMc5vHlHZfnqjawj/RsM2S6Q6fa9T9CnY1Nz7DYBG3yZJyCPFsrgU0
+# 5s9ljqfsSptpFdUh9R4ce+L71SWDLM2x/1MFLLHAMbXsEp8KloEGtaDULnxtfS2t
+# YhfuKGqRXoEfDPAMnIdTvQPh3GHQ4SjkkBARHL0MY75alhGTKHWjC2aLVOo8obKI
+# Bk8hfnFDUf/EyVw4uTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUw
+# DQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n
+# dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y
+# YXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhv
+# cml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkG
+# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
+# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9z
+# b2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
+# ggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg
+# 4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aO
+# RmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41
+# JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5
+# LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL
+# 64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9
+# QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj
+# 0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqE
+# UUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0
+# kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435
+# UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB
+# 3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTE
+# mr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwG
+# A1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93
+# d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNV
+# HSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNV
+# HQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo
+# 0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29m
+# dC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5j
+# cmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jv
+# c29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDAN
+# BgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4
+# sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th54
+# 2DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRX
+# ud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBew
+# VIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0
+# DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+Cljd
+# QDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFr
+# DZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFh
+# bHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7n
+# tdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+
+# oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6Fw
+# ZvKhggLXMIICQAIBATCCAQChgdikgdUwgdIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
+# ZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
+# dGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046M0JENC00Qjgw
+# LTY5QzMxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoB
+# ATAHBgUrDgMCGgMVACGlCa3ketyeuey7bJNpWkMuiCcQoIGDMIGApH4wfDELMAkG
+# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
+# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9z
+# b2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEFBQACBQDlsRtBMCIY
+# DzIwMjIwMjEyMDEyODMzWhgPMjAyMjAyMTMwMTI4MzNaMHcwPQYKKwYBBAGEWQoE
+# ATEvMC0wCgIFAOWxG0ECAQAwCgIBAAICDbMCAf8wBwIBAAICEW8wCgIFAOWybMEC
+# AQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEK
+# MAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQCImCpEJ2AlAWBBkDABmkqIh1kM
+# LPDyea3b7evhOk+YSwXCzxnBIXuppujFT3tnk7w0p0a5YS9uwqbDM/M6rAUMBAR0
+# boHamumEITNF5nVh0rlYyRZQ3WraVD2YPhouUINQavmS8ueYoh6r3HeM9QPBAnNB
+# vv7GDrZ637+2Dfe60jGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYD
+# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
+# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
+# IFBDQSAyMDEwAhMzAAABibS/hjCEHEuPAAEAAAGJMA0GCWCGSAFlAwQCAQUAoIIB
+# SjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEIL86
+# iebNndOm+CAgIp67s6y+HI1wHdhaMPILGf48RtXXMIH6BgsqhkiG9w0BCRACLzGB
+# 6jCB5zCB5DCBvQQgZndHMdxQV1VsbpWHOTHqWEycvcRJm7cY69l/UmT8j0UwgZgw
+# gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
+# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
+# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAYm0v4YwhBxL
+# jwABAAABiTAiBCDET+l3keOFFxaIqOZWSSuWNO774Ng/t5pe3p4QXoKcvjANBgkq
+# hkiG9w0BAQsFAASCAgADYrNFej7RbihwGcC0jF+cTik+HJog++dPEDXeIyBB+2pw
+# 23hC5KaX9H05ZknluIq2oxf2MLpKL+gA+76T3k5PnzPNJFDogUn5eFIIsMRpNF0h
+# MtPWoPJWYFK2odvKz1HwsuqHRg6hO//NwORcv4xPeAWEFO5+DOXzZKKp/BVDGe/D
+# c++y9/l41qpz/F2c3a1lugdqnZz7ZeoaQ8/JMlwrmMbciqcAytCn9A59EWJ1xYd/
+# DaDhQ5Rd8hkcckuxJksjWf6URmc91cb4Jdatkyupq3dDGwCkjGNd2xetrOpqMLOZ
+# quoDONSgc9rGrhkf3xgKKVRhLg9bxd3f2oQ0IsOBg2AC5td1eqp6TILc0gei2E3I
+# uEAW1d+KXDnajvQmvQkaFHr5wEocTTLgrDglOPPhEaEumSTJS7jKFzUKHiBU005p
+# CgQ1So2WJ2RqFx0ppez1N1AFczOVLFllK3WGPLkDsN1GgT0nFfoqvs1WKkzyb2d2
+# /v6PVER9xGky7LCu62dhsJCAFUbxF2dJxaC5ofrl98VaO/z72J9on9BTz+eCtcJ9
+# rDIpqktGeL02f6+4zctFCyi2wgm6eh8kKvRlAPmN4/MNt9pWHtEV//xFGzGeDajr
+# diRhDoMZwsuon4QwS8b2YcKMoZ6gZ2lZah3960fTTmvBTBNqeBtR94KWCy0C0A==
+# SIG # End signature block
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/tools/uninstall.ps1 b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/tools/uninstall.ps1
new file mode 100644
index 0000000000..17fd920015
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Package/tools/uninstall.ps1
@@ -0,0 +1,279 @@
+param($installPath, $toolsPath, $package, $project)
+
+if($project.Object.SupportsPackageDependencyResolution)
+{
+ if($project.Object.SupportsPackageDependencyResolution())
+ {
+ # Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it.
+ return
+ }
+}
+
+$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve
+
+foreach($analyzersPath in $analyzersPaths)
+{
+ # Uninstall the language agnostic analyzers.
+ if (Test-Path $analyzersPath)
+ {
+ foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll)
+ {
+ if($project.Object.AnalyzerReferences)
+ {
+ $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName)
+ }
+ }
+ }
+}
+
+# $project.Type gives the language name like (C# or VB.NET)
+$languageFolder = ""
+if($project.Type -eq "C#")
+{
+ $languageFolder = "cs"
+}
+if($project.Type -eq "VB.NET")
+{
+ $languageFolder = "vb"
+}
+if($languageFolder -eq "")
+{
+ return
+}
+
+foreach($analyzersPath in $analyzersPaths)
+{
+ # Uninstall language specific analyzers.
+ $languageAnalyzersPath = join-path $analyzersPath $languageFolder
+ if (Test-Path $languageAnalyzersPath)
+ {
+ foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll)
+ {
+ if($project.Object.AnalyzerReferences)
+ {
+ try
+ {
+ $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName)
+ }
+ catch
+ {
+
+ }
+ }
+ }
+ }
+}
+# SIG # Begin signature block
+# MIInugYJKoZIhvcNAQcCoIInqzCCJ6cCAQExDzANBglghkgBZQMEAgEFADB5Bgor
+# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
+# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDC68wb97fg0QGL
+# yXrxJhYfmibzcOh8caqC0uZprfczDaCCDYEwggX/MIID56ADAgECAhMzAAACUosz
+# qviV8znbAAAAAAJSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
+# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
+# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
+# bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMjU5WhcNMjIwOTAxMTgzMjU5WjB0MQsw
+# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
+# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
+# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+# AQDQ5M+Ps/X7BNuv5B/0I6uoDwj0NJOo1KrVQqO7ggRXccklyTrWL4xMShjIou2I
+# sbYnF67wXzVAq5Om4oe+LfzSDOzjcb6ms00gBo0OQaqwQ1BijyJ7NvDf80I1fW9O
+# L76Kt0Wpc2zrGhzcHdb7upPrvxvSNNUvxK3sgw7YTt31410vpEp8yfBEl/hd8ZzA
+# v47DCgJ5j1zm295s1RVZHNp6MoiQFVOECm4AwK2l28i+YER1JO4IplTH44uvzX9o
+# RnJHaMvWzZEpozPy4jNO2DDqbcNs4zh7AWMhE1PWFVA+CHI/En5nASvCvLmuR/t8
+# q4bc8XR8QIZJQSp+2U6m2ldNAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
+# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUNZJaEUGL2Guwt7ZOAu4efEYXedEw
+# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
+# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDY3NTk3MB8GA1UdIwQYMBaAFEhu
+# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
+# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
+# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
+# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
+# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFkk3
+# uSxkTEBh1NtAl7BivIEsAWdgX1qZ+EdZMYbQKasY6IhSLXRMxF1B3OKdR9K/kccp
+# kvNcGl8D7YyYS4mhCUMBR+VLrg3f8PUj38A9V5aiY2/Jok7WZFOAmjPRNNGnyeg7
+# l0lTiThFqE+2aOs6+heegqAdelGgNJKRHLWRuhGKuLIw5lkgx9Ky+QvZrn/Ddi8u
+# TIgWKp+MGG8xY6PBvvjgt9jQShlnPrZ3UY8Bvwy6rynhXBaV0V0TTL0gEx7eh/K1
+# o8Miaru6s/7FyqOLeUS4vTHh9TgBL5DtxCYurXbSBVtL1Fj44+Od/6cmC9mmvrti
+# yG709Y3Rd3YdJj2f3GJq7Y7KdWq0QYhatKhBeg4fxjhg0yut2g6aM1mxjNPrE48z
+# 6HWCNGu9gMK5ZudldRw4a45Z06Aoktof0CqOyTErvq0YjoE4Xpa0+87T/PVUXNqf
+# 7Y+qSU7+9LtLQuMYR4w3cSPjuNusvLf9gBnch5RqM7kaDtYWDgLyB42EfsxeMqwK
+# WwA+TVi0HrWRqfSx2olbE56hJcEkMjOSKz3sRuupFCX3UroyYf52L+2iVTrda8XW
+# esPG62Mnn3T8AuLfzeJFuAbfOSERx7IFZO92UPoXE1uEjL5skl1yTZB3MubgOA4F
+# 8KoRNhviFAEST+nG8c8uIsbZeb08SeYQMqjVEmkwggd6MIIFYqADAgECAgphDpDS
+# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
+# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
+# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
+# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
+# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
+# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
+# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
+# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
+# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
+# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
+# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
+# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
+# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
+# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
+# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
+# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
+# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
+# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
+# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
+# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
+# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
+# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
+# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
+# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
+# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
+# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
+# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
+# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
+# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
+# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
+# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
+# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
+# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
+# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
+# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
+# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
+# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
+# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
+# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
+# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIZjzCCGYsCAQEwgZUwfjELMAkG
+# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
+# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
+# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN
+# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
+# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgF1ypFyzl
+# AvvWGVCeXczrfpXmJNm9vpyjcwd4y4ivfqowQgYKKwYBBAGCNwIBDDE0MDKgFIAS
+# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
+# BgkqhkiG9w0BAQEFAASCAQAvi2rSDkhC82RJ4uqq/0WbHkOkzq1hrF6HxneBTNj8
+# KX+niFtee3CYVfWaSAQ6xvOiLupRX3fsSfhabRQ+Jl8k28voGrTK1OC906OO3tUN
+# jdmv1PooWdxJNt2EbzQrap5Ui9KTUv4mJ4c836HAVMBPCJiq5NwmzAHfbgBxCaYq
+# +hupIf+gk8vuNB1bltILgNmU/smJt9OuGqSo5TrFajzb+35SqjnCz9JtAtbPNZvA
+# X9N37UPhITOecceAQmrHiEPbA7eu6VDp6VPjPfCEO7a+frWa83chEd4qzyou9xu5
+# 3gnj7Ro8nFDnGyUe0+0oCaYGXO9fbIMN1HG2IZg5suj5oYIXGTCCFxUGCisGAQQB
+# gjcDAwExghcFMIIXAQYJKoZIhvcNAQcCoIIW8jCCFu4CAQMxDzANBglghkgBZQME
+# AgEFADCCAVkGCyqGSIb3DQEJEAEEoIIBSASCAUQwggFAAgEBBgorBgEEAYRZCgMB
+# MDEwDQYJYIZIAWUDBAIBBQAEIH+XBTHuyyHZnIXrFWIe64WLvHx5GUFMCM6A56T1
+# KwBtAgZh/WKJ52UYEzIwMjIwMjExMTkwMzQwLjU0OFowBIACAfSggdikgdUwgdIx
+# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
+# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1p
+# Y3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhh
+# bGVzIFRTUyBFU046M0JENC00QjgwLTY5QzMxJTAjBgNVBAMTHE1pY3Jvc29mdCBU
+# aW1lLVN0YW1wIFNlcnZpY2WgghFoMIIHFDCCBPygAwIBAgITMwAAAYm0v4YwhBxL
+# jwABAAABiTANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMK
+# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
+# IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0Eg
+# MjAxMDAeFw0yMTEwMjgxOTI3NDFaFw0yMzAxMjYxOTI3NDFaMIHSMQswCQYDVQQG
+# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
+# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQg
+# SXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg
+# RVNOOjNCRDQtNEI4MC02OUMzMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt
+# cCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvQZXxZFm
+# a6plmuOyvNpV8xONOwcYolZG/BjyZWGSk5JOGaLyrKId5VxVHWHlsmJE4Svnzsdp
+# sKmVx8otONveIUFvSceEZp8VXmu5m1fu8L7c+3lwXcibjccqtEvtQslokQVx0r+L
+# 54abrNDarwFG73IaRidIS1i9c+unJ8oYyhDRLrCysFAVxyQhPNZkWK7Z8/VGukaK
+# LAWHXCh/+R53h42gFL+9/mAALxzCXXuofi8f/XKCm7xNwVc1hONCCz6oq94AufzV
+# NkkIW4brUQgYpCcJm9U0XNmQvtropYDn9UtY8YQ0NKenXPtdgLHdQ8Nnv3igErKL
+# rWI0a5n5jjdKfwk+8mvakqdZmlOseeOS1XspQNJAK1uZllAITcnQZOcO5ofjOQ33
+# ujWckAXdz+/x3o7l4AU/TSOMzGZMwhUdtVwC3dSbItpSVFgnjM2COEJ9zgCadvOi
+# rGDLN471jZI2jClkjsJTdgPk343TQA4JFvds/unZq0uLr+niZ3X44OBx2x+gVlln
+# 2c4UbZXNueA4yS1TJGbbJFIILAmTUA9Auj5eISGTbNiyWx79HnCOTar39QEKozm4
+# LnTmDXy0/KI/H/nYZGKuTHfckP28wQS06rD+fDS5xLwcRMCW92DkHXmtbhGyRilB
+# OL5LxZelQfxt54wl4WUC0AdAEolPekODwO8CAwEAAaOCATYwggEyMB0GA1UdDgQW
+# BBSXbx+zR1p4IIAeguA6rHKkrfl7UDAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJl
+# pxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
+# b20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAx
+# MCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3
+# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3Rh
+# bXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoG
+# CCsGAQUFBwMIMA0GCSqGSIb3DQEBCwUAA4ICAQCOtLdpWUI4KwfLLrfaKrLB92Dq
+# bAspGWM41TaO4Jl+sHxPo522uu3GKQCjmkRWreHtlfyy9kOk7LWax3k3ke8Gtfet
+# fbh7qH0LeV2XOWg39BOnHf6mTcZq7FYSZZch1JDQjc98+Odlow+oWih0Dbt4CV/e
+# 19ZcE+1n1zzWkskUEd0f5jPIUis33p+vkY8szduAtCcIcPFUhI8Hb5alPUAPMjGz
+# wKb7NIKbnf8j8cP18As5IveckF0oh1cw63RY/vPK62LDYdpi7WnG2ObvngfWVKtw
+# iwTI4jHj2cO9q37HDe/PPl216gSpUZh0ap24mKmMDfcKp1N4mEdsxz4oseOrPYeF
+# sHHWJFJ6Aivvqn70KTeJpp5r+DxSqbeSy0mxIUOq/lAaUxgNSQVUX26t8r+fciko
+# fKv23WHrtRV3t7rVTsB9YzrRaiikmz68K5HWdt9MqULxPQPo+ppZ0LRqkOae466+
+# UKRY0JxWtdrMc5vHlHZfnqjawj/RsM2S6Q6fa9T9CnY1Nz7DYBG3yZJyCPFsrgU0
+# 5s9ljqfsSptpFdUh9R4ce+L71SWDLM2x/1MFLLHAMbXsEp8KloEGtaDULnxtfS2t
+# YhfuKGqRXoEfDPAMnIdTvQPh3GHQ4SjkkBARHL0MY75alhGTKHWjC2aLVOo8obKI
+# Bk8hfnFDUf/EyVw4uTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUw
+# DQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n
+# dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y
+# YXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhv
+# cml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkG
+# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
+# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9z
+# b2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
+# ggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg
+# 4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aO
+# RmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41
+# JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5
+# LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL
+# 64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9
+# QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj
+# 0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqE
+# UUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0
+# kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435
+# UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB
+# 3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTE
+# mr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwG
+# A1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93
+# d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNV
+# HSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNV
+# HQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo
+# 0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29m
+# dC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5j
+# cmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jv
+# c29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDAN
+# BgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4
+# sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th54
+# 2DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRX
+# ud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBew
+# VIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0
+# DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+Cljd
+# QDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFr
+# DZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFh
+# bHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7n
+# tdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+
+# oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6Fw
+# ZvKhggLXMIICQAIBATCCAQChgdikgdUwgdIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
+# ZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
+# dGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046M0JENC00Qjgw
+# LTY5QzMxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoB
+# ATAHBgUrDgMCGgMVACGlCa3ketyeuey7bJNpWkMuiCcQoIGDMIGApH4wfDELMAkG
+# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
+# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9z
+# b2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEFBQACBQDlsRtBMCIY
+# DzIwMjIwMjEyMDEyODMzWhgPMjAyMjAyMTMwMTI4MzNaMHcwPQYKKwYBBAGEWQoE
+# ATEvMC0wCgIFAOWxG0ECAQAwCgIBAAICDbMCAf8wBwIBAAICEW8wCgIFAOWybMEC
+# AQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEK
+# MAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQCImCpEJ2AlAWBBkDABmkqIh1kM
+# LPDyea3b7evhOk+YSwXCzxnBIXuppujFT3tnk7w0p0a5YS9uwqbDM/M6rAUMBAR0
+# boHamumEITNF5nVh0rlYyRZQ3WraVD2YPhouUINQavmS8ueYoh6r3HeM9QPBAnNB
+# vv7GDrZ637+2Dfe60jGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYD
+# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
+# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
+# IFBDQSAyMDEwAhMzAAABibS/hjCEHEuPAAEAAAGJMA0GCWCGSAFlAwQCAQUAoIIB
+# SjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEIKY2
+# Onyhltfi0+oc/UMKaXc0H6Ckw2gGK1/qmjRZNiXnMIH6BgsqhkiG9w0BCRACLzGB
+# 6jCB5zCB5DCBvQQgZndHMdxQV1VsbpWHOTHqWEycvcRJm7cY69l/UmT8j0UwgZgw
+# gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
+# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
+# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAYm0v4YwhBxL
+# jwABAAABiTAiBCDET+l3keOFFxaIqOZWSSuWNO774Ng/t5pe3p4QXoKcvjANBgkq
+# hkiG9w0BAQsFAASCAgB7AQ0Dv3muHoNAt+cccMfYk23lHgh8LGBitCSFwu0q7ufv
+# sXkoaIpwW0U0GikWhQoCH0U38SuzVbafg49FiE6ftkjOtiE03PwPYi1S6NSoDdaV
+# kUuvjz3OcuN1IHg3CyLn2dO8xbUlWCUfgoWhI1nax9ch7wT4Sw8RdmGKdYTZoZmq
+# vPXFRtDyZdmJDMDbTql/Brye8oEsDMoYKMmEYhY1t9TlusnWfUbxuBnyMqg/FkBy
+# QF78WFfT8mygMqUGmINxPGT6daxqmq3nfAC2vOtLT4DplNYMEymfDceJzBhb8VCT
+# UHc2CWK0qKT+eqwn30NBkwh//8aNHlXaA9Yq/9k2y+axIGdxFfG+X0stipRRpEXb
+# xCFm7FPD5/S4ddBH829yEZLZ4XTwSZ6YS/d3mFzu5rgZl3UhjOJPXx40GQtUiDP4
+# XQZ/wW3154X/KtTypv62/Hl+CiMUrsO7MXtgwClfbJ3osg+zlpJgdraetVgmAUc1
+# mjz2GCYX7rIliGkAJREKn4rV2MZzuGLEpTjz9dB+1Xp9Ndi9q3jQgs6k3IDIUube
+# YjPFFuPmFWRyi6oPTXmc4ExtTIewPvrOhwQ5q4ysxylkXoTS+UQt94BY2SvR+TMu
+# 6doU+0Y73xsO8Zz+lREh3fjBsDbPAgOV5989X6bmkJEEIwIK8LYgqvyED8XXTg==
+# SIG # End signature block
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/ModularPipelines.Analyzers.Test.csproj b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/ModularPipelines.Analyzers.Test.csproj
new file mode 100644
index 0000000000..2b8d00c19c
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/ModularPipelines.Analyzers.Test.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net6.0
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/ModularPipelinesAnalyzersUnitTest.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/ModularPipelinesAnalyzersUnitTest.cs
new file mode 100644
index 0000000000..b5b5264879
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/ModularPipelinesAnalyzersUnitTest.cs
@@ -0,0 +1,112 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using VerifyCS = ModularPipelines.Analyzers.Test.Verifiers.CSharpCodeFixVerifier<
+ ModularPipelines.Analyzers.MissingDependsOnAttributeAnalyzer,
+ ModularPipelines.Analyzers.MissingDependsOnAttributeCodeFixProvider>;
+
+namespace ModularPipelines.Analyzers.Test;
+
+[TestClass]
+public class ModularPipelinesAnalyzersUnitTest
+{
+ private const string BadModuleSource = @"
+#nullable enable
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.Examples.Modules;
+
+public class Module1 : Module
+{
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Delay(1, cancellationToken);
+ return null;
+ }
+}
+
+public class Module2 : Module
+{
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ var module1 = await {|#0:GetModule()|};
+ return null;
+ }
+}";
+
+ private const string FixedModuleSource = @"#nullable enable
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+using ModularPipelines.Attributes;
+
+namespace ModularPipelines.Examples.Modules;
+public class Module1 : Module
+{
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Delay(1, cancellationToken);
+ return null;
+ }
+}
+
+[DependsOn]
+public class Module2 : Module
+{
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ var module1 = await GetModule();
+ return null;
+ }
+}";
+
+ //No diagnostics expected to show up
+ [TestMethod]
+ public async Task Empty_Source()
+ {
+ var test = @"";
+
+ await VerifyCS.VerifyAnalyzerAsync(test);
+ }
+
+ //No diagnostics expected to show up
+ [TestMethod]
+ public async Task Good_Source()
+ {
+ var test = @"";
+
+ await VerifyCS.VerifyAnalyzerAsync(FixedModuleSource);
+ }
+
+ //Diagnostic and CodeFix both triggered and checked for
+ [TestMethod]
+ public async Task AnalyzerIsTriggered()
+ {
+ var expected = VerifyCS.Diagnostic(MissingDependsOnAttributeAnalyzer.DiagnosticId).WithArguments("Module1").WithLocation(0);
+
+ await VerifyCS.VerifyAnalyzerAsync(BadModuleSource, expected);
+ }
+
+ [TestMethod]
+ public async Task CodeFixWorks()
+ {
+ if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+ {
+ // This fails on Linux only due to different line endings
+ // Is there a way around that?
+ return;
+ }
+
+ var expected = VerifyCS.Diagnostic(MissingDependsOnAttributeAnalyzer.DiagnosticId).WithArguments("Module1").WithLocation(0);
+
+ await VerifyCS.VerifyCodeFixAsync(BadModuleSource, expected, FixedModuleSource);
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Net.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Net.cs
new file mode 100644
index 0000000000..9272494ed3
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Net.cs
@@ -0,0 +1,16 @@
+using Microsoft.CodeAnalysis.Testing;
+
+namespace ModularPipelines.Analyzers.Test;
+
+public class Net
+{
+ private static readonly Lazy _lazyNet60 = new(() =>
+ new ReferenceAssemblies(
+ "net6.0",
+ new PackageIdentity(
+ "Microsoft.NETCore.App.Ref",
+ "6.0.19"),
+ Path.Combine("ref", "net6.0")));
+
+ public static ReferenceAssemblies Net60 => _lazyNet60.Value;
+}
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier.cs
new file mode 100644
index 0000000000..79574c4dad
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier.cs
@@ -0,0 +1,32 @@
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+
+namespace ModularPipelines.Analyzers.Test.Verifiers;
+
+public static partial class CSharpAnalyzerVerifier
+ where TAnalyzer : DiagnosticAnalyzer, new()
+{
+ public class Test : CSharpAnalyzerTest
+ {
+ public Test()
+ {
+ SolutionTransforms.Add((solution, projectId) =>
+ {
+ var project = solution.GetProject(projectId);
+
+ var compilationOptions = project.CompilationOptions;
+
+ var parseOptions = project.ParseOptions as CSharpParseOptions;
+
+ compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings));
+
+ solution = solution.WithProjectCompilationOptions(projectId, compilationOptions)
+ .WithProjectParseOptions(projectId, parseOptions.WithLanguageVersion(LanguageVersion.Preview));
+
+ return solution;
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1.cs
new file mode 100644
index 0000000000..cfa0092145
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1.cs
@@ -0,0 +1,35 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+
+namespace ModularPipelines.Analyzers.Test.Verifiers;
+
+public static partial class CSharpAnalyzerVerifier
+ where TAnalyzer : DiagnosticAnalyzer, new()
+{
+ ///
+ public static DiagnosticResult Diagnostic()
+ => CSharpAnalyzerVerifier.Diagnostic();
+
+ ///
+ public static DiagnosticResult Diagnostic(string diagnosticId)
+ => CSharpAnalyzerVerifier.Diagnostic(diagnosticId);
+
+ ///
+ public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
+ => CSharpAnalyzerVerifier.Diagnostic(descriptor);
+
+ ///
+ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
+ {
+ var test = new Test
+ {
+ TestCode = source,
+ };
+
+ test.ExpectedDiagnostics.AddRange(expected);
+ await test.RunAsync(CancellationToken.None);
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeFixVerifier.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeFixVerifier.cs
new file mode 100644
index 0000000000..1f7b45652e
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeFixVerifier.cs
@@ -0,0 +1,34 @@
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+
+namespace ModularPipelines.Analyzers.Test.Verifiers;
+
+public static partial class CSharpCodeFixVerifier
+ where TAnalyzer : DiagnosticAnalyzer, new()
+ where TCodeFix : CodeFixProvider, new()
+{
+ public class Test : CSharpCodeFixTest
+ {
+ public Test()
+ {
+ SolutionTransforms.Add((solution, projectId) =>
+ {
+ var project = solution.GetProject(projectId);
+
+ var compilationOptions = project.CompilationOptions;
+
+ var parseOptions = project.ParseOptions as CSharpParseOptions;
+
+ compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings));
+
+ solution = solution.WithProjectCompilationOptions(projectId, compilationOptions)
+ .WithProjectParseOptions(projectId, parseOptions.WithLanguageVersion(LanguageVersion.Preview));
+
+ return solution;
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeFixVerifier`2.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeFixVerifier`2.cs
new file mode 100644
index 0000000000..6a643f5c46
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeFixVerifier`2.cs
@@ -0,0 +1,70 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+using ModularPipelines.Context;
+
+namespace ModularPipelines.Analyzers.Test.Verifiers;
+
+public static partial class CSharpCodeFixVerifier
+ where TAnalyzer : DiagnosticAnalyzer, new()
+ where TCodeFix : CodeFixProvider, new()
+{
+ ///
+ public static DiagnosticResult Diagnostic()
+ => CSharpCodeFixVerifier.Diagnostic();
+
+ ///
+ public static DiagnosticResult Diagnostic(string diagnosticId)
+ => CSharpCodeFixVerifier.Diagnostic(diagnosticId);
+
+ ///
+ public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
+ => CSharpCodeFixVerifier.Diagnostic(descriptor);
+
+ ///
+ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
+ {
+ var test = new Test
+ {
+ TestCode = source,
+ ReferenceAssemblies = Net.Net60,
+ CodeActionValidationMode = CodeActionValidationMode.SemanticStructure,
+ TestState =
+ {
+ AdditionalReferences = { typeof(IModuleContext).Assembly.Location },
+ },
+ };
+
+ test.ExpectedDiagnostics.AddRange(expected);
+ await test.RunAsync(CancellationToken.None);
+ }
+
+ ///
+ public static async Task VerifyCodeFixAsync(string source, string fixedSource)
+ => await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource);
+
+ ///
+ public static async Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource)
+ => await VerifyCodeFixAsync(source, new[] { expected }, fixedSource);
+
+ ///
+ public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource)
+ {
+ var test = new Test
+ {
+ TestCode = source,
+ FixedCode = fixedSource,
+ ReferenceAssemblies = Net.Net60,
+ TestState =
+ {
+ AdditionalReferences = { typeof(IModuleContext).Assembly.Location },
+ }
+ };
+
+ test.ExpectedDiagnostics.AddRange(expected);
+ await test.RunAsync(CancellationToken.None);
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeRefactoringVerifier.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeRefactoringVerifier.cs
new file mode 100644
index 0000000000..caa42e0d74
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeRefactoringVerifier.cs
@@ -0,0 +1,32 @@
+using Microsoft.CodeAnalysis.CodeRefactorings;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+
+namespace ModularPipelines.Analyzers.Test.Verifiers;
+
+public static partial class CSharpCodeRefactoringVerifier
+ where TCodeRefactoring : CodeRefactoringProvider, new()
+{
+ public class Test : CSharpCodeRefactoringTest
+ {
+ public Test()
+ {
+ SolutionTransforms.Add((solution, projectId) =>
+ {
+ var project = solution.GetProject(projectId);
+
+ var compilationOptions = project.CompilationOptions;
+
+ var parseOptions = project.ParseOptions as CSharpParseOptions;
+
+ compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings));
+
+ solution = solution.WithProjectCompilationOptions(projectId, compilationOptions)
+ .WithProjectParseOptions(projectId, parseOptions.WithLanguageVersion(LanguageVersion.Preview));
+
+ return solution;
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeRefactoringVerifier`1.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeRefactoringVerifier`1.cs
new file mode 100644
index 0000000000..cfff2fda97
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpCodeRefactoringVerifier`1.cs
@@ -0,0 +1,33 @@
+using Microsoft.CodeAnalysis.CodeRefactorings;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace ModularPipelines.Analyzers.Test.Verifiers;
+
+public static partial class CSharpCodeRefactoringVerifier
+ where TCodeRefactoring : CodeRefactoringProvider, new()
+{
+ ///
+ public static async Task VerifyRefactoringAsync(string source, string fixedSource)
+ {
+ await VerifyRefactoringAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource);
+ }
+
+ ///
+ public static async Task VerifyRefactoringAsync(string source, DiagnosticResult expected, string fixedSource)
+ {
+ await VerifyRefactoringAsync(source, new[] { expected }, fixedSource);
+ }
+
+ ///
+ public static async Task VerifyRefactoringAsync(string source, DiagnosticResult[] expected, string fixedSource)
+ {
+ var test = new Test
+ {
+ TestCode = source,
+ FixedCode = fixedSource,
+ };
+
+ test.ExpectedDiagnostics.AddRange(expected);
+ await test.RunAsync(CancellationToken.None);
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpVerifierHelper.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpVerifierHelper.cs
new file mode 100644
index 0000000000..51d7f250a8
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Test/Verifiers/CSharpVerifierHelper.cs
@@ -0,0 +1,32 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace ModularPipelines.Analyzers.Test.Verifiers;
+
+internal static class CSharpVerifierHelper
+{
+ ///
+ /// By default, the compiler reports diagnostics for nullable reference types at
+ /// , and the analyzer test framework defaults to only validating
+ /// diagnostics at . This map contains all compiler diagnostic IDs
+ /// related to nullability mapped to , which is then used to enable all
+ /// of these warnings for default validation during analyzer and code fix tests.
+ ///
+ internal static ImmutableDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler();
+
+ private static ImmutableDictionary GetNullableWarningsFromCompiler()
+ {
+ string[] args = { "/warnaserror:nullable", "-p:LangVersion=preview" };
+ var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory);
+ var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions;
+
+ // Workaround for https://github.com/dotnet/roslyn/issues/41610
+ nullableWarnings = nullableWarnings
+ .SetItem("CS8632", ReportDiagnostic.Error)
+ .SetItem("CS8669", ReportDiagnostic.Error)
+ .SetItem("CS8652", ReportDiagnostic.Suppress);
+
+ return nullableWarnings;
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Vsix/ModularPipelines.Analyzers.Vsix.csproj b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Vsix/ModularPipelines.Analyzers.Vsix.csproj
new file mode 100644
index 0000000000..e9527d98bd
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Vsix/ModularPipelines.Analyzers.Vsix.csproj
@@ -0,0 +1,48 @@
+
+
+
+
+
+ net472
+ ModularPipelines.Analyzers.Vsix
+ ModularPipelines.Analyzers.Vsix
+
+
+
+ false
+ false
+ false
+ false
+ false
+ false
+ Roslyn
+
+
+
+
+
+
+
+ Program
+ $(DevEnvDir)devenv.exe
+ /rootsuffix $(VSSDKTargetPlatformRegRootSuffix)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Vsix/source.extension.vsixmanifest b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Vsix/source.extension.vsixmanifest
new file mode 100644
index 0000000000..8f07296f21
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers.Vsix/source.extension.vsixmanifest
@@ -0,0 +1,24 @@
+
+
+
+
+ ModularPipelines.Analyzers
+ This is a sample diagnostic extension for the .NET Compiler Platform ("Roslyn").
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers/AnalyzerReleases.Shipped.md b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/AnalyzerReleases.Shipped.md
new file mode 100644
index 0000000000..cb396d5be7
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/AnalyzerReleases.Shipped.md
@@ -0,0 +1,7 @@
+## Release 1.0
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|--------------------
+MissingDependsOnAttribute | Usage | Error | MissingDependsOnAttributeAnalyzer
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers/AnalyzerReleases.Unshipped.md b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/AnalyzerReleases.Unshipped.md
new file mode 100644
index 0000000000..6a2a74bcf8
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,4 @@
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|--------------------
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers/MissingDependsOnAttributeAnalyzer.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/MissingDependsOnAttributeAnalyzer.cs
new file mode 100644
index 0000000000..39a5aae015
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/MissingDependsOnAttributeAnalyzer.cs
@@ -0,0 +1,134 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System.Collections.Immutable;
+
+namespace ModularPipelines.Analyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class MissingDependsOnAttributeAnalyzer : DiagnosticAnalyzer
+{
+ public const string DiagnosticId = "MissingDependsOnAttribute";
+
+ // You can change these strings in the Resources.resx file. If you do not want your analyzer to be localize-able, you can use regular strings for Title and MessageFormat.
+ // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Localizing%20Analyzers.md for more on localization
+ private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources));
+ private const string Category = "Usage";
+
+ private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterSyntaxNodeAction(AnalyzeMissingDependsOnAttributes, SyntaxKind.InvocationExpression);
+ }
+
+ private void AnalyzeMissingDependsOnAttributes(SyntaxNodeAnalysisContext context)
+ {
+ if (context.Node is not InvocationExpressionSyntax invocationExpressionSyntax)
+ {
+ return;
+ }
+
+ if (invocationExpressionSyntax.Expression is not GenericNameSyntax genericNameSyntax)
+ {
+ return;
+ }
+
+ if (genericNameSyntax.Identifier.ValueText is not ("GetModule" or "WaitForModule"))
+ {
+ return;
+ }
+
+ var genericArgument = genericNameSyntax.TypeArgumentList.Arguments.First();
+
+ var genericArgumentSymbol = context.SemanticModel.GetSymbolInfo(genericArgument).Symbol;
+
+ if (genericArgumentSymbol is not INamedTypeSymbol namedTypeSymbol)
+ {
+ return;
+ }
+
+ var classContainingGetModuleCallSyntax = GetClassDeclarationSyntax(invocationExpressionSyntax);
+
+ if (classContainingGetModuleCallSyntax is null)
+ {
+ return;
+ }
+
+ var classSymbol = context.SemanticModel.GetDeclaredSymbol(classContainingGetModuleCallSyntax);
+
+ if (classSymbol is null)
+ {
+ return;
+ }
+
+ var attributes = classSymbol.GetAttributes();
+
+ if (!attributes.Any(x => IsDependsOnAttributeFor(x, namedTypeSymbol)))
+ {
+ var properties = new Dictionary
+ {
+ ["Name"] = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)
+ }.ToImmutableDictionary();
+
+ context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(), properties, namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)));
+ }
+ }
+
+ private ClassDeclarationSyntax? GetClassDeclarationSyntax(InvocationExpressionSyntax invocationExpressionSyntax)
+ {
+ var parent = invocationExpressionSyntax.Parent;
+
+ while (parent is not null)
+ {
+ if (parent is ClassDeclarationSyntax classDeclarationSyntax)
+ {
+ return classDeclarationSyntax;
+ }
+
+ parent = parent.Parent;
+ }
+
+ return null;
+ }
+
+ private bool IsDependsOnAttributeFor(AttributeData attributeData, INamedTypeSymbol namedTypeSymbol)
+ {
+ var attributeClassName = attributeData.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+
+ if (string.IsNullOrEmpty(attributeClassName))
+ {
+ return false;
+ }
+
+ if (!attributeClassName!.StartsWith("global::ModularPipelines.Attributes.DependsOnAttribute"))
+ {
+ return false;
+ }
+
+ if (attributeData.AttributeClass!.IsGenericType)
+ {
+ return SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass.TypeArguments.First(), namedTypeSymbol);
+ }
+
+ return attributeData.ConstructorArguments.Any(x =>
+ {
+ var argumentValue = x.Value;
+
+ if (argumentValue is INamedTypeSymbol argumentNamedTypeSymbol)
+ {
+ return SymbolEqualityComparer.Default.Equals(argumentNamedTypeSymbol, namedTypeSymbol);
+ }
+
+ return false;
+ });
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers/ModularPipelines.Analyzers.csproj b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/ModularPipelines.Analyzers.csproj
new file mode 100644
index 0000000000..8166bff8d6
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/ModularPipelines.Analyzers.csproj
@@ -0,0 +1,25 @@
+
+
+
+ netstandard2.0
+ false
+
+
+ *$(MSBuildProjectFile)*
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers/Resources.Designer.cs b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/Resources.Designer.cs
new file mode 100644
index 0000000000..80043af14b
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/Resources.Designer.cs
@@ -0,0 +1,89 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace ModularPipelines.Analyzers {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ModularPipelines.Analyzers.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Missing DependsOn Attribute.
+ ///
+ internal static string AnalyzerDescription {
+ get {
+ return ResourceManager.GetString("AnalyzerDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to This Module should have a DependsOn attribute for {0}.
+ ///
+ internal static string AnalyzerMessageFormat {
+ get {
+ return ResourceManager.GetString("AnalyzerMessageFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Missing DependsOn Attribute.
+ ///
+ internal static string AnalyzerTitle {
+ get {
+ return ResourceManager.GetString("AnalyzerTitle", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/ModularPipelines.Analyzers/ModularPipelines.Analyzers/Resources.resx b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/Resources.resx
new file mode 100644
index 0000000000..e588c52f46
--- /dev/null
+++ b/ModularPipelines.Analyzers/ModularPipelines.Analyzers/Resources.resx
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Missing DependsOn Attribute
+ An optional longer localizable description of the diagnostic.
+
+
+ This Module should have a DependsOn attribute for {0}
+ The format-able message the diagnostic displays.
+
+
+ Missing DependsOn Attribute
+ The title of the diagnostic.
+
+
\ No newline at end of file
diff --git a/ModularPipelines.Azure.Pipelines/AzurePipeline.cs b/ModularPipelines.Azure.Pipelines/AzurePipeline.cs
new file mode 100644
index 0000000000..ee8ec3c472
--- /dev/null
+++ b/ModularPipelines.Azure.Pipelines/AzurePipeline.cs
@@ -0,0 +1,11 @@
+namespace ModularPipelines.Azure.Pipelines;
+
+public class AzurePipeline : IAzurePipeline
+{
+ public AzurePipeline(AzurePipelineVariables variables)
+ {
+ Variables = variables;
+ }
+
+ public AzurePipelineVariables Variables { get; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Azure.Pipelines/AzurePipelineAgentVariables.cs b/ModularPipelines.Azure.Pipelines/AzurePipelineAgentVariables.cs
new file mode 100644
index 0000000000..2d0ca67fd0
--- /dev/null
+++ b/ModularPipelines.Azure.Pipelines/AzurePipelineAgentVariables.cs
@@ -0,0 +1,20 @@
+namespace ModularPipelines.Azure.Pipelines;
+
+public class AzurePipelineAgentVariables : AzurePipelineVariableBase
+{
+ protected override string Prefix => "Agent";
+
+ public string BuildDirectory => Get("BuildDirectory")!;
+ public string ContainerMapping => Get("ContainerMapping")!;
+ public string HomeDirectory => Get("HomeDirectory")!;
+ public string Id => Get("Id")!;
+ public string JobName => Get("JobName")!;
+ public string JobStatus => Get("JobStatus")!;
+ public string MachineName => Get("MachineName")!;
+ public string Name => Get("Name")!;
+ public string OS => Get("OS")!;
+ public string OSArchitecture => Get("OSArchitecture")!;
+ public string TempDirectory => Get("TempDirectory")!;
+ public string ToolsDirectory => Get("ToolsDirectory")!;
+ public string WorkFolder => Get("WorkFolder")!;
+}
\ No newline at end of file
diff --git a/ModularPipelines.Azure.Pipelines/AzurePipelineVariableBase.cs b/ModularPipelines.Azure.Pipelines/AzurePipelineVariableBase.cs
new file mode 100644
index 0000000000..3154aabf57
--- /dev/null
+++ b/ModularPipelines.Azure.Pipelines/AzurePipelineVariableBase.cs
@@ -0,0 +1,9 @@
+namespace ModularPipelines.Azure.Pipelines;
+
+public abstract class AzurePipelineVariableBase
+{
+ protected abstract string Prefix { get; }
+
+ protected string? Get(string variableName) => Environment.GetEnvironmentVariable(ToEnvironmentVariableName(variableName));
+ private string ToEnvironmentVariableName(string variableName) => Prefix?.ToUpperInvariant() + "_" + variableName.ToUpperInvariant().Replace('.', '_');
+}
\ No newline at end of file
diff --git a/ModularPipelines.Azure.Pipelines/AzurePipelineVariables.cs b/ModularPipelines.Azure.Pipelines/AzurePipelineVariables.cs
new file mode 100644
index 0000000000..ddd61e0e1c
--- /dev/null
+++ b/ModularPipelines.Azure.Pipelines/AzurePipelineVariables.cs
@@ -0,0 +1,11 @@
+namespace ModularPipelines.Azure.Pipelines;
+
+public class AzurePipelineVariables
+{
+ public AzurePipelineAgentVariables Agent { get; }
+
+ public AzurePipelineVariables(AzurePipelineAgentVariables agent)
+ {
+ Agent = agent;
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Azure.Pipelines/Extensions/AzurePipelineExtensions.cs b/ModularPipelines.Azure.Pipelines/Extensions/AzurePipelineExtensions.cs
new file mode 100644
index 0000000000..bce632a5e0
--- /dev/null
+++ b/ModularPipelines.Azure.Pipelines/Extensions/AzurePipelineExtensions.cs
@@ -0,0 +1,28 @@
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using ModularPipelines.Context;
+using ModularPipelines.Engine;
+
+namespace ModularPipelines.Azure.Pipelines.Extensions;
+
+public static class AzurePipelineExtensions
+{
+#pragma warning disable CA2255
+ [ModuleInitializer]
+#pragma warning restore CA2255
+ public static void RegisterAzurePipelineContext()
+ {
+ ServiceContextRegistry.RegisterContext(collection => RegisterAzurePipelineContext(collection));
+ }
+
+ public static IServiceCollection RegisterAzurePipelineContext(this IServiceCollection services)
+ {
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ return services;
+ }
+
+ public static IAzurePipeline AzurePipeline(this IModuleContext context) => (IAzurePipeline) context.ServiceProvider.GetRequiredService();
+}
\ No newline at end of file
diff --git a/ModularPipelines.Azure.Pipelines/IAzurePipeline.cs b/ModularPipelines.Azure.Pipelines/IAzurePipeline.cs
new file mode 100644
index 0000000000..3223342674
--- /dev/null
+++ b/ModularPipelines.Azure.Pipelines/IAzurePipeline.cs
@@ -0,0 +1,6 @@
+namespace ModularPipelines.Azure.Pipelines;
+
+public interface IAzurePipeline
+{
+ public AzurePipelineVariables Variables { get; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Command/ModularPipelines.Command.csproj b/ModularPipelines.Azure.Pipelines/ModularPipelines.Azure.Pipelines.csproj
similarity index 100%
rename from ModularPipelines.Command/ModularPipelines.Command.csproj
rename to ModularPipelines.Azure.Pipelines/ModularPipelines.Azure.Pipelines.csproj
diff --git a/ModularPipelines.Build/ModularPipelines.Build.csproj b/ModularPipelines.Build/ModularPipelines.Build.csproj
index ba86c2d18e..de417a12cc 100644
--- a/ModularPipelines.Build/ModularPipelines.Build.csproj
+++ b/ModularPipelines.Build/ModularPipelines.Build.csproj
@@ -11,12 +11,17 @@
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/ModularPipelines.Build/Modules/CleanModule.cs b/ModularPipelines.Build/Modules/CleanModule.cs
deleted file mode 100644
index d1ab12b048..0000000000
--- a/ModularPipelines.Build/Modules/CleanModule.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using CliWrap.Buffered;
-using ModularPipelines.Attributes;
-using ModularPipelines.Context;
-using ModularPipelines.DotNet.Extensions;
-using ModularPipelines.DotNet.Options;
-using ModularPipelines.Models;
-using ModularPipelines.Modules;
-
-namespace ModularPipelines.Build.Modules;
-
-[DependsOn]
-public class CleanModule : Module>
-{
- public CleanModule(IModuleContext context) : base(context)
- {
- }
-
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
- {
- var results = new List();
-
- foreach (var projectFile in Context.Environment
- .GitRootDirectory!
- .GetFiles(file => file.Path.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)))
- {
- results.Add(await Context.DotNet().Clean(new DotNetOptions
- {
- TargetPath = projectFile.Path
- }, cancellationToken));
- }
-
- return results;
- }
-}
\ No newline at end of file
diff --git a/ModularPipelines.Build/Modules/LocalMachine/AddLocalNugetSourceModule.cs b/ModularPipelines.Build/Modules/LocalMachine/AddLocalNugetSourceModule.cs
index 2eab603eec..d4b5d3b34b 100644
--- a/ModularPipelines.Build/Modules/LocalMachine/AddLocalNugetSourceModule.cs
+++ b/ModularPipelines.Build/Modules/LocalMachine/AddLocalNugetSourceModule.cs
@@ -1,6 +1,7 @@
using CliWrap.Buffered;
using ModularPipelines.Attributes;
using ModularPipelines.Context;
+using ModularPipelines.Exceptions;
using ModularPipelines.Models;
using ModularPipelines.Modules;
using ModularPipelines.NuGet.Extensions;
@@ -11,17 +12,17 @@ namespace ModularPipelines.Build.Modules.LocalMachine;
[DependsOn]
public class AddLocalNugetSourceModule : Module
{
- public AddLocalNugetSourceModule(IModuleContext context) : base(context)
+ protected override Task ShouldIgnoreFailures(IModuleContext context, Exception exception)
{
+ return Task.FromResult(exception is CommandException commandException &&
+ commandException.CommandResult.StandardOutput.Contains("The name specified has already been added to the list of available package sources"));
}
- public override bool IgnoreFailures => true;
-
- protected override async Task?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
var localNugetPathResult = await GetModule();
- return await Context.NuGet()
+ return await context.NuGet()
.AddSource(new NuGetSourceOptions(new Uri(localNugetPathResult.Value!), "ModularPipelinesLocalNuGet"));
}
}
\ No newline at end of file
diff --git a/ModularPipelines.Build/Modules/LocalMachine/CreateLocalNugetFolderModule.cs b/ModularPipelines.Build/Modules/LocalMachine/CreateLocalNugetFolderModule.cs
index 8ba2a1b9e8..d941970897 100644
--- a/ModularPipelines.Build/Modules/LocalMachine/CreateLocalNugetFolderModule.cs
+++ b/ModularPipelines.Build/Modules/LocalMachine/CreateLocalNugetFolderModule.cs
@@ -10,13 +10,9 @@ namespace ModularPipelines.Build.Modules.LocalMachine;
[DependsOn]
public class CreateLocalNugetFolderModule : Module
{
- public CreateLocalNugetFolderModule(IModuleContext context) : base(context)
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
- }
-
- protected override async Task?> ExecuteAsync(CancellationToken cancellationToken)
- {
- var userAppData = Context.FileSystem.GetFolder(Environment.SpecialFolder.ApplicationData).Path;
+ var userAppData = context.FileSystem.GetFolder(Environment.SpecialFolder.ApplicationData).Path;
var localNugetRepositoryFolderPath = Path.Combine(userAppData, "ModularPipelines", "LocalNuget");
@@ -27,7 +23,7 @@ public CreateLocalNugetFolderModule(IModuleContext context) : base(context)
await Task.Yield();
- Context.Logger.LogInformation("Local NuGet Repository Path: {Path}", localNugetRepositoryFolderPath);
+ context.Logger.LogInformation("Local NuGet Repository Path: {Path}", localNugetRepositoryFolderPath);
return localNugetRepositoryFolderPath;
}
diff --git a/ModularPipelines.Build/Modules/LocalMachine/UploadPackagesToLocalNuGetModule.cs b/ModularPipelines.Build/Modules/LocalMachine/UploadPackagesToLocalNuGetModule.cs
index 9762f57279..83c990158b 100644
--- a/ModularPipelines.Build/Modules/LocalMachine/UploadPackagesToLocalNuGetModule.cs
+++ b/ModularPipelines.Build/Modules/LocalMachine/UploadPackagesToLocalNuGetModule.cs
@@ -12,28 +12,24 @@ namespace ModularPipelines.Build.Modules.LocalMachine;
[DependsOn]
public class UploadPackagesToLocalNuGetModule : Module>
{
- public UploadPackagesToLocalNuGetModule(IModuleContext context) : base(context)
- {
- }
-
- protected override async Task InitialiseAsync()
+ protected override async Task OnBeforeExecute(IModuleContext context)
{
var packagePaths = await GetModule();
foreach (var packagePath in packagePaths.Value!)
{
- Context.Logger.LogInformation("Uploading {File}", packagePath);
+ context.Logger.LogInformation("Uploading {File}", packagePath);
}
- await base.InitialiseAsync();
+ await base.OnBeforeExecute(context);
}
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
var localRepoLocation = await GetModule();
var packagePaths = await GetModule();
- return await Context.NuGet()
- .UploadPackage(new NuGetUploadOptions(packagePaths.Value!, new Uri(localRepoLocation.Value!)));
+ return await context.NuGet()
+ .UploadPackages(new NuGetUploadOptions(packagePaths.Value!, new Uri(localRepoLocation.Value!)));
}
}
\ No newline at end of file
diff --git a/ModularPipelines.Build/Modules/NugetVersionGeneratorModule.cs b/ModularPipelines.Build/Modules/NugetVersionGeneratorModule.cs
new file mode 100644
index 0000000000..b304370b67
--- /dev/null
+++ b/ModularPipelines.Build/Modules/NugetVersionGeneratorModule.cs
@@ -0,0 +1,19 @@
+using ModularPipelines.Context;
+using ModularPipelines.Git.Extensions;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.Build.Modules;
+
+public class NugetVersionGeneratorModule : Module
+{
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+
+ var branchName = context.Git().Information.BranchName;
+ var commitDate = context.Git().Information.LastCommitDateTime;
+
+ return $"{GitVersionInformation.Major}.{GitVersionInformation.Minor}.{GitVersionInformation.Patch}-{GitVersionInformation.PreReleaseLabel}-{GitVersionInformation.CommitsSinceVersionSource}";
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Build/Modules/PackProjectsModule.cs b/ModularPipelines.Build/Modules/PackProjectsModule.cs
index 91f93c1240..878a7bf165 100644
--- a/ModularPipelines.Build/Modules/PackProjectsModule.cs
+++ b/ModularPipelines.Build/Modules/PackProjectsModule.cs
@@ -1,8 +1,6 @@
using CliWrap.Buffered;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
using ModularPipelines.Attributes;
-using ModularPipelines.Build.Settings;
using ModularPipelines.Context;
using ModularPipelines.DotNet.Extensions;
using ModularPipelines.DotNet.Options;
@@ -15,29 +13,26 @@ namespace ModularPipelines.Build.Modules;
[DependsOn]
public class PackProjectsModule : Module>
{
- private readonly IOptions _options;
-
- public PackProjectsModule(IModuleContext context, IOptions options) : base(context)
- {
- _options = options;
- }
-
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
var results = new List();
- foreach (var unitTestProjectFile in Context.Environment
- .GitRootDirectory!
- .GetFiles(GetProjectsPredicate))
+ var packageVersion = await GetModule();
+
+ var unitTestProjectFiles = context.Environment
+ .GitRootDirectory!
+ .GetFiles(f => GetProjectsPredicate(f, context));
+
+ foreach (var unitTestProjectFile in unitTestProjectFiles)
{
- results.Add(await Context.DotNet().Pack(new DotNetOptions
+ results.Add(await context.DotNet().Pack(new DotNetOptions
{
TargetPath = unitTestProjectFile.Path,
Configuration = Configuration.Release,
ExtraArguments = new List
{
- $"/p:PackageVersion={_options.Value.Version}",
- $"/p:Version={_options.Value.Version}"
+ $"/p:PackageVersion={packageVersion.Value}",
+ $"/p:Version={packageVersion.Value}"
}
}, cancellationToken));
}
@@ -45,7 +40,7 @@ public PackProjectsModule(IModuleContext context, IOptions opti
return results;
}
- private bool GetProjectsPredicate(File file)
+ private bool GetProjectsPredicate(File file, IModuleContext context)
{
var path = file.Path;
if (!path.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase))
@@ -64,7 +59,7 @@ private bool GetProjectsPredicate(File file)
return false;
}
- Context.Logger.LogInformation("Found File: {File}", path);
+ context.Logger.LogInformation("Found File: {File}", path);
return true;
}
diff --git a/ModularPipelines.Build/Modules/PackageFilesRemovalModule.cs b/ModularPipelines.Build/Modules/PackageFilesRemovalModule.cs
index a6a9a44653..3bb7074497 100644
--- a/ModularPipelines.Build/Modules/PackageFilesRemovalModule.cs
+++ b/ModularPipelines.Build/Modules/PackageFilesRemovalModule.cs
@@ -1,23 +1,17 @@
-using ModularPipelines.Attributes;
using ModularPipelines.Context;
using ModularPipelines.Models;
using ModularPipelines.Modules;
namespace ModularPipelines.Build.Modules;
-[DependsOn]
public class PackageFilesRemovalModule : Module
{
- public PackageFilesRemovalModule(IModuleContext context) : base(context)
+ protected override Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
- }
-
- protected override Task>?> ExecuteAsync(CancellationToken cancellationToken)
- {
- var packageFiles = Context.FileSystem.GetFiles(Context.Environment.GitRootDirectory!.Path,
+ var packageFiles = context.FileSystem.GetFiles(context.Environment.GitRootDirectory!.Path,
SearchOption.AllDirectories,
path =>
- path.Extension == ".nupkg");
+ path.Extension is ".nupkg");
foreach (var packageFile in packageFiles)
{
diff --git a/ModularPipelines.Build/Modules/PackagePathsParserModule.cs b/ModularPipelines.Build/Modules/PackagePathsParserModule.cs
index e8e7dee8c5..7c07d68659 100644
--- a/ModularPipelines.Build/Modules/PackagePathsParserModule.cs
+++ b/ModularPipelines.Build/Modules/PackagePathsParserModule.cs
@@ -8,11 +8,7 @@ namespace ModularPipelines.Build.Modules;
[DependsOn]
public class PackagePathsParserModule : Module>
{
- public PackagePathsParserModule(IModuleContext context) : base(context)
- {
- }
-
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
var packPackagesModuleResult = await GetModule();
diff --git a/ModularPipelines.Build/Modules/RunUnitTestsModule.cs b/ModularPipelines.Build/Modules/RunUnitTestsModule.cs
index fedd37bfee..3d2521b1df 100644
--- a/ModularPipelines.Build/Modules/RunUnitTestsModule.cs
+++ b/ModularPipelines.Build/Modules/RunUnitTestsModule.cs
@@ -1,5 +1,6 @@
using CliWrap.Buffered;
using ModularPipelines.Context;
+using ModularPipelines.DotNet;
using ModularPipelines.DotNet.Extensions;
using ModularPipelines.DotNet.Options;
using ModularPipelines.Models;
@@ -7,24 +8,21 @@
namespace ModularPipelines.Build.Modules;
-public class RunUnitTestsModule : Module>
+public class RunUnitTestsModule : Module>
{
- public RunUnitTestsModule(IModuleContext context) : base(context)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
- }
-
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
- {
- var results = new List();
+ var results = new List();
- foreach (var unitTestProjectFile in Context.Environment
+ foreach (var unitTestProjectFile in context.Environment
.GitRootDirectory!
.GetFiles(file => file.Path.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)
&& file.Path.Contains("UnitTests", StringComparison.OrdinalIgnoreCase)))
{
- results.Add(await Context.DotNet().Test(new DotNetOptions
+ results.Add(await context.DotNet().Test(new DotNetOptions
{
- TargetPath = unitTestProjectFile.Path
+ TargetPath = unitTestProjectFile.Path,
+ LogOutput = false
}, cancellationToken));
}
diff --git a/ModularPipelines.Build/Modules/UploadPackagesToNugetModule.cs b/ModularPipelines.Build/Modules/UploadPackagesToNugetModule.cs
index 1aa1adb39a..e47b460424 100644
--- a/ModularPipelines.Build/Modules/UploadPackagesToNugetModule.cs
+++ b/ModularPipelines.Build/Modules/UploadPackagesToNugetModule.cs
@@ -17,30 +17,30 @@ public class UploadPackagesToNugetModule : Module>
{
private readonly IOptions _options;
- public UploadPackagesToNugetModule(IModuleContext context, IOptions options) : base(context)
+ public UploadPackagesToNugetModule(IOptions options)
{
ArgumentNullException.ThrowIfNull(options.Value.ApiKey);
_options = options;
}
- protected override async Task InitialiseAsync()
+ protected override async Task OnBeforeExecute(IModuleContext context)
{
var packagePaths = await GetModule();
foreach (var packagePath in packagePaths.Value!)
{
- Context.Logger.LogInformation("Uploading {File}", packagePath);
+ context.Logger.LogInformation("Uploading {File}", packagePath);
}
- await base.InitialiseAsync();
+ await base.OnBeforeExecute(context);
}
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
var packagePaths = await GetModule();
- return await Context.NuGet()
- .UploadPackage(new NuGetUploadOptions(packagePaths.Value!, new Uri("https://api.nuget.org/v3/index.json"))
+ return await context.NuGet()
+ .UploadPackages(new NuGetUploadOptions(packagePaths.Value!, new Uri("https://api.nuget.org/v3/index.json"))
{
ApiKey = _options.Value.ApiKey!
});
diff --git a/ModularPipelines.Build/MyModuleHooks.cs b/ModularPipelines.Build/MyModuleHooks.cs
index 2fb5ebdb8e..3145de4413 100644
--- a/ModularPipelines.Build/MyModuleHooks.cs
+++ b/ModularPipelines.Build/MyModuleHooks.cs
@@ -3,20 +3,19 @@
using ModularPipelines.Interfaces;
using ModularPipelines.Modules;
-namespace ModularPipelines.Build
+namespace ModularPipelines.Build;
+
+public class MyModuleHooks : IPipelineModuleHooks
{
- public class MyModuleHooks : IPipelineModuleHooks
+ public Task OnBeforeModuleStartAsync(IModuleContext moduleContext, ModuleBase module)
{
- public Task OnBeforeModuleStartAsync(IModuleContext moduleContext, IModule module)
- {
- moduleContext.Logger.LogInformation("{Module} is starting", module.GetType().Name);
- return Task.CompletedTask;
- }
+ moduleContext.Logger.LogInformation("{Module} is starting", module.GetType().Name);
+ return Task.CompletedTask;
+ }
- public Task OnBeforeModuleEndAsync(IModuleContext moduleContext, IModule module)
- {
- moduleContext.Logger.LogInformation("{Module} finished after {Elapsed}", module.GetType().Name, module.Duration);
- return Task.CompletedTask;
- }
+ public Task OnBeforeModuleEndAsync(IModuleContext moduleContext, ModuleBase module)
+ {
+ moduleContext.Logger.LogInformation("{Module} finished after {Elapsed}", module.GetType().Name, module.Duration);
+ return Task.CompletedTask;
}
}
\ No newline at end of file
diff --git a/ModularPipelines.Build/Program.cs b/ModularPipelines.Build/Program.cs
index 923f9bbff6..1f2dbedb47 100644
--- a/ModularPipelines.Build/Program.cs
+++ b/ModularPipelines.Build/Program.cs
@@ -5,11 +5,8 @@
using ModularPipelines.Build.Modules;
using ModularPipelines.Build.Modules.LocalMachine;
using ModularPipelines.Build.Settings;
-using ModularPipelines.Command.Extensions;
-using ModularPipelines.DotNet.Extensions;
using ModularPipelines.Extensions;
using ModularPipelines.Host;
-using ModularPipelines.NuGet.Extensions;
var modules = await PipelineHostBuilder.Create()
.ConfigureAppConfiguration((context, builder) =>
@@ -21,15 +18,10 @@
.ConfigureServices((context, collection) =>
{
collection.Configure(context.Configuration.GetSection("NuGet"));
- collection.Configure(context.Configuration.GetSection("Publish"));
- collection.RegisterCommandContext()
- .RegisterDotNetContext()
- .RegisterNuGetContext();
-
collection.AddModule()
+ .AddModule()
.AddModule()
- .AddModule()
.AddModule()
.AddModule()
.AddPipelineModuleHooks();
diff --git a/ModularPipelines.Build/Settings/PublishSettings.cs b/ModularPipelines.Build/Settings/PublishSettings.cs
deleted file mode 100644
index 72cd38d65f..0000000000
--- a/ModularPipelines.Build/Settings/PublishSettings.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace ModularPipelines.Build.Settings;
-
-public record PublishSettings
-{
- public string? Version { get; set; }
-}
\ No newline at end of file
diff --git a/ModularPipelines.Build/appsettings.json b/ModularPipelines.Build/appsettings.json
index 89ac486b4a..de8c4f98c6 100644
--- a/ModularPipelines.Build/appsettings.json
+++ b/ModularPipelines.Build/appsettings.json
@@ -6,9 +6,6 @@
"Microsoft": "Information"
}
},
- "Publish": {
- "Version": "0.0.1-alpha03"
- },
"NuGet": {
"ApiKey": "Override from Secret Source"
}
diff --git a/ModularPipelines.Cmd/Cmd.cs b/ModularPipelines.Cmd/Cmd.cs
new file mode 100644
index 0000000000..1819e2db25
--- /dev/null
+++ b/ModularPipelines.Cmd/Cmd.cs
@@ -0,0 +1,30 @@
+using CliWrap.Buffered;
+using ModularPipelines.Cmd.Models;
+using ModularPipelines.Context;
+using ModularPipelines.Extensions;
+
+namespace ModularPipelines.Cmd;
+
+public class Cmd : ICmd
+{
+ private readonly IModuleContext _context;
+
+ public Cmd(IModuleContext context)
+ {
+ _context = context;
+ }
+
+ public Task Script(CmdScriptOptions options, CancellationToken cancellationToken = default)
+ {
+ var arguments = new List { "/c" };
+
+ if (!options.Echo)
+ {
+ arguments.Add("/q");
+ }
+
+ arguments.Add(options.Script);
+
+ return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("cmd", arguments), cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Cmd/Extensions/CmdExtensions.cs b/ModularPipelines.Cmd/Extensions/CmdExtensions.cs
new file mode 100644
index 0000000000..b72bb51c94
--- /dev/null
+++ b/ModularPipelines.Cmd/Extensions/CmdExtensions.cs
@@ -0,0 +1,27 @@
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using ModularPipelines.Context;
+using ModularPipelines.Engine;
+
+namespace ModularPipelines.Cmd.Extensions;
+
+public static class CmdExtensions
+{
+#pragma warning disable CA2255
+ [ModuleInitializer]
+#pragma warning restore CA2255
+ public static void RegisterCmdContext()
+ {
+ ServiceContextRegistry.RegisterContext(collection => RegisterCmdContext(collection));
+ }
+
+ public static IServiceCollection RegisterCmdContext(this IServiceCollection services)
+ {
+ services.TryAddSingleton();
+
+ return services;
+ }
+
+ public static ICmd Cmd(this IModuleContext context) => (ICmd) context.ServiceProvider.GetRequiredService();
+}
\ No newline at end of file
diff --git a/ModularPipelines.Cmd/ICmd.cs b/ModularPipelines.Cmd/ICmd.cs
new file mode 100644
index 0000000000..bd01747400
--- /dev/null
+++ b/ModularPipelines.Cmd/ICmd.cs
@@ -0,0 +1,9 @@
+using CliWrap.Buffered;
+using ModularPipelines.Cmd.Models;
+
+namespace ModularPipelines.Cmd;
+
+public interface ICmd
+{
+ Task Script(CmdScriptOptions options, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/ModularPipelines.Cmd/Models/CmdScriptOptions.cs b/ModularPipelines.Cmd/Models/CmdScriptOptions.cs
new file mode 100644
index 0000000000..cb4510d1b9
--- /dev/null
+++ b/ModularPipelines.Cmd/Models/CmdScriptOptions.cs
@@ -0,0 +1,8 @@
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Cmd.Models;
+
+public record CmdScriptOptions(string Script) : CommandEnvironmentOptions
+{
+ public bool Echo { get; init; } = true;
+}
\ No newline at end of file
diff --git a/ModularPipelines.Installer/ModularPipelines.Installer.csproj b/ModularPipelines.Cmd/ModularPipelines.Cmd.csproj
similarity index 77%
rename from ModularPipelines.Installer/ModularPipelines.Installer.csproj
rename to ModularPipelines.Cmd/ModularPipelines.Cmd.csproj
index 29eda92bbe..4decd268f3 100644
--- a/ModularPipelines.Installer/ModularPipelines.Installer.csproj
+++ b/ModularPipelines.Cmd/ModularPipelines.Cmd.csproj
@@ -7,7 +7,6 @@
-
diff --git a/ModularPipelines.Command/Command.cs b/ModularPipelines.Command/Command.cs
deleted file mode 100644
index 2d2c5f934b..0000000000
--- a/ModularPipelines.Command/Command.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System.Collections.ObjectModel;
-using CliWrap;
-using CliWrap.Buffered;
-using ModularPipelines.Command.Options;
-
-namespace ModularPipelines.Command;
-
-public class Command : ICommand
-{
- public async Task Of(CommandOptions options, CancellationToken cancellationToken = default)
- {
- var command = Cli.Wrap(options.Command);
-
- if (options.WorkingDirectory != null)
- {
- command = command.WithWorkingDirectory(options.WorkingDirectory);
- }
-
- if (options.EnvironmentVariables != null)
- {
- command = command.WithEnvironmentVariables(options.EnvironmentVariables);
- }
-
- var result = await command
- .ExecuteBufferedAsync(cancellationToken: cancellationToken);
-
- return result;
- }
-
- public async Task UsingCommandLineTool(CommandLineToolOptions options, CancellationToken cancellationToken = default)
- {
- var parsedArgs = string.Equals(options.Arguments?.ElementAtOrDefault(0), options.Tool)
- ? options.Arguments?.Skip(1) : options.Arguments;
-
- var command = Cli.Wrap(options.Tool)
- .WithArguments(parsedArgs ?? Array.Empty());
-
- if (options.WorkingDirectory != null)
- {
- command = command.WithWorkingDirectory(options.WorkingDirectory);
- }
-
- if (options.EnvironmentVariables != null)
- {
- command = command.WithEnvironmentVariables(new ReadOnlyDictionary(options.EnvironmentVariables));
- }
-
- var result = await command
- .ExecuteBufferedAsync(cancellationToken: cancellationToken);
-
- return result;
- }
-}
\ No newline at end of file
diff --git a/ModularPipelines.Command/Extensions/CommandExtensions.cs b/ModularPipelines.Command/Extensions/CommandExtensions.cs
deleted file mode 100644
index 897cfdd0f4..0000000000
--- a/ModularPipelines.Command/Extensions/CommandExtensions.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
-using ModularPipelines.Context;
-
-namespace ModularPipelines.Command.Extensions;
-
-public static class CommandExtensions
-{
- public static IServiceCollection RegisterCommandContext(this IServiceCollection services)
- {
- services.TryAddSingleton();
- return services;
- }
-
- public static ICommand Command(this IModuleContext context) => context.Get()!;
-}
\ No newline at end of file
diff --git a/ModularPipelines.Command/Options/CommandLineToolOptions.cs b/ModularPipelines.Command/Options/CommandLineToolOptions.cs
deleted file mode 100644
index 85a5b87c73..0000000000
--- a/ModularPipelines.Command/Options/CommandLineToolOptions.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace ModularPipelines.Command.Options;
-
-public record CommandLineToolOptions(string Tool)
-{
- public IEnumerable? Arguments { get; init; }
- public IDictionary? EnvironmentVariables { get; init; }
- public string? WorkingDirectory { get; init; }
-}
\ No newline at end of file
diff --git a/ModularPipelines.Command/Options/CommandOptions.cs b/ModularPipelines.Command/Options/CommandOptions.cs
deleted file mode 100644
index 0c7f9cf8a1..0000000000
--- a/ModularPipelines.Command/Options/CommandOptions.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace ModularPipelines.Command.Options;
-
-public record CommandOptions(string Command)
-{
- public IReadOnlyDictionary? EnvironmentVariables { get; init; }
- public string? WorkingDirectory { get; init; }
-}
\ No newline at end of file
diff --git a/ModularPipelines.Docker/Docker.cs b/ModularPipelines.Docker/Docker.cs
new file mode 100644
index 0000000000..5c0873a10f
--- /dev/null
+++ b/ModularPipelines.Docker/Docker.cs
@@ -0,0 +1,77 @@
+using ModularPipelines.Context;
+using ModularPipelines.Docker.Options;
+using ModularPipelines.Extensions;
+
+namespace ModularPipelines.Docker;
+
+public class Docker : IDocker
+{
+ private readonly ICommand _command;
+
+ public Docker(ICommand command)
+ {
+ _command = command;
+ }
+
+ public async Task Login(DockerLoginOptions dockerLoginOptions)
+ {
+ var arguments = new List
+ {
+ "login",
+ $"--username={dockerLoginOptions.Username}",
+ $"--password={dockerLoginOptions.Password}"
+ };
+
+ arguments.AddNonNullOrEmpty(dockerLoginOptions.Server?.AbsolutePath);
+
+ await _command.UsingCommandLineTool(dockerLoginOptions.ToCommandLineToolOptions("docker", arguments));
+ }
+
+ public async Task BuildFromDockerfile(DockerBuildOptions dockerBuildOptions)
+ {
+ var workingDirectory =
+ dockerBuildOptions.DockerfileFolder.Path == dockerBuildOptions.WorkingDirectory ? "." : dockerBuildOptions.DockerfileFolder.Path;
+
+ var arguments = new List
+ {
+ "build",
+ workingDirectory,
+ };
+
+ arguments.AddNonNullOrEmptyArgumentWithSwitch("-t", dockerBuildOptions.Tag);
+
+ arguments.AddRangeNonNullOrEmptyArgumentWithSwitch("--build-arg", dockerBuildOptions.BuildArguments);
+
+ arguments.AddNonNullOrEmpty(dockerBuildOptions.Dockerfile?.Path);
+
+ await _command.UsingCommandLineTool(dockerBuildOptions.ToCommandLineToolOptions("docker", arguments));
+ }
+
+ public Task Logout(DockerOptions? options = null)
+ {
+ options ??= new DockerOptions();
+
+ return _command.UsingCommandLineTool(options.ToCommandLineToolOptions("docker", new[] { "logout" }));
+ }
+
+ public Task Push(DockerPushOptions dockerPushOptions)
+ {
+ var arguments = new List
+ {
+ "push",
+ $"--disable-content-trust={dockerPushOptions.DisableContentTrust.ToString().ToLower()}",
+ $"{dockerPushOptions.Name}:{dockerPushOptions.Tag}"
+ };
+
+ return _command.UsingCommandLineTool(dockerPushOptions.ToCommandLineToolOptions("docker", arguments));
+ }
+
+ public async Task Version(DockerArgumentOptions? dockerArgumentOptions = null)
+ {
+ dockerArgumentOptions ??= new DockerArgumentOptions();
+
+ var result = await _command.UsingCommandLineTool(dockerArgumentOptions.ToCommandLineToolOptions("docker", new []{ "version" }));
+
+ return result.StandardOutput;
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Docker/DockerBuildOptions.cs b/ModularPipelines.Docker/DockerBuildOptions.cs
new file mode 100644
index 0000000000..711511199a
--- /dev/null
+++ b/ModularPipelines.Docker/DockerBuildOptions.cs
@@ -0,0 +1,16 @@
+using ModularPipelines.FileSystem;
+using ModularPipelines.Options;
+using File = ModularPipelines.FileSystem.File;
+
+namespace ModularPipelines.Docker;
+
+public record DockerBuildOptions(Folder DockerfileFolder) : CommandLineToolOptions("docker")
+{
+ public string? Name { get; init; }
+
+ public string? Tag { get; init; }
+
+ public IEnumerable? BuildArguments { get; init; }
+
+ public File? Dockerfile { get; init; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Docker/DockerLoginOptions.cs b/ModularPipelines.Docker/DockerLoginOptions.cs
new file mode 100644
index 0000000000..4813f0b8b0
--- /dev/null
+++ b/ModularPipelines.Docker/DockerLoginOptions.cs
@@ -0,0 +1,8 @@
+using ModularPipelines.Docker.Options;
+
+namespace ModularPipelines.Docker;
+
+public record DockerLoginOptions(string Username, string Password) : DockerOptions
+{
+ public Uri? Server { get; init; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Docker/DockerPushOptions.cs b/ModularPipelines.Docker/DockerPushOptions.cs
new file mode 100644
index 0000000000..f0d0a167bf
--- /dev/null
+++ b/ModularPipelines.Docker/DockerPushOptions.cs
@@ -0,0 +1,8 @@
+using ModularPipelines.Docker.Options;
+
+namespace ModularPipelines.Docker;
+
+public record DockerPushOptions(string Name, string Tag) : DockerOptions
+{
+ public bool DisableContentTrust { get; init; } = true;
+};
\ No newline at end of file
diff --git a/ModularPipelines.Docker/IDocker.cs b/ModularPipelines.Docker/IDocker.cs
new file mode 100644
index 0000000000..6641916e2f
--- /dev/null
+++ b/ModularPipelines.Docker/IDocker.cs
@@ -0,0 +1,12 @@
+using ModularPipelines.Docker.Options;
+
+namespace ModularPipelines.Docker;
+
+public interface IDocker
+{
+ Task Login(DockerLoginOptions dockerLoginOptions);
+ Task BuildFromDockerfile(DockerBuildOptions dockerBuildOptions);
+ Task Logout(DockerOptions? options = null);
+ Task Push(DockerPushOptions dockerPushOptions);
+ Task Version(DockerArgumentOptions? dockerArgumentOptions = null);
+}
\ No newline at end of file
diff --git a/ModularPipelines.Docker/ModularPipelines.Docker.csproj b/ModularPipelines.Docker/ModularPipelines.Docker.csproj
new file mode 100644
index 0000000000..b1997dacac
--- /dev/null
+++ b/ModularPipelines.Docker/ModularPipelines.Docker.csproj
@@ -0,0 +1,11 @@
+
+
+
+ net6.0
+
+
+
+
+
+
+
diff --git a/ModularPipelines.Docker/Options/DockerArgumentOptions.cs b/ModularPipelines.Docker/Options/DockerArgumentOptions.cs
new file mode 100644
index 0000000000..f8a7f18f51
--- /dev/null
+++ b/ModularPipelines.Docker/Options/DockerArgumentOptions.cs
@@ -0,0 +1,5 @@
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Docker.Options;
+
+public record DockerArgumentOptions() : CommandLineToolOptions("docker");
\ No newline at end of file
diff --git a/ModularPipelines.Docker/Options/DockerOptions.cs b/ModularPipelines.Docker/Options/DockerOptions.cs
new file mode 100644
index 0000000000..ef2e825b3b
--- /dev/null
+++ b/ModularPipelines.Docker/Options/DockerOptions.cs
@@ -0,0 +1,5 @@
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Docker.Options;
+
+public record DockerOptions : CommandEnvironmentOptions;
\ No newline at end of file
diff --git a/ModularPipelines.DotNet/DotNet.cs b/ModularPipelines.DotNet/DotNet.cs
index 631bfe2436..df0f101808 100644
--- a/ModularPipelines.DotNet/DotNet.cs
+++ b/ModularPipelines.DotNet/DotNet.cs
@@ -1,18 +1,21 @@
using CliWrap.Buffered;
-using ModularPipelines.Command.Extensions;
-using ModularPipelines.Command.Options;
using ModularPipelines.Context;
using ModularPipelines.DotNet.Options;
+using ModularPipelines.Extensions;
+using ModularPipelines.Options;
+using File = ModularPipelines.FileSystem.File;
namespace ModularPipelines.DotNet;
public class DotNet : IDotNet
{
- public IModuleContext Context { get; }
+ private readonly IModuleContext _context;
+ private readonly ITrxParser _trxParser;
- public DotNet(IModuleContext context)
+ public DotNet(IModuleContext context, ITrxParser trxParser)
{
- Context = context;
+ _context = context;
+ _trxParser = trxParser;
}
public Task Restore(DotNetOptions options, CancellationToken cancellationToken = default)
@@ -40,11 +43,40 @@ public Task Clean(DotNetOptions options, CancellationToke
return RunCommand(ToDotNetCommandOptions("clean", options), cancellationToken);
}
- public Task Test(DotNetOptions options, CancellationToken cancellationToken = default)
+ public async Task Test(DotNetOptions options, CancellationToken cancellationToken = default)
{
- return RunCommand(ToDotNetCommandOptions("test", options), cancellationToken);
+ var trxFilePath = Path.GetTempFileName();
+ var argumentsWithLogger = options.ExtraArguments?.ToList() ?? new List();
+ argumentsWithLogger.Add("--logger");
+ argumentsWithLogger.Add($"trx;logfilename={trxFilePath}");
+
+ var optionsWithLogger = options with
+ {
+ ExtraArguments = argumentsWithLogger
+ };
+
+ var command = await RunCommand(ToDotNetCommandOptions("test", optionsWithLogger), cancellationToken);
+
+ var trxContents = await _context.FileSystem.GetFile(trxFilePath).ReadAsync();
+
+ return _trxParser.ParseTestResult(trxContents);
}
-
+
+ public Task Version(CommandEnvironmentOptions? options, CancellationToken cancellationToken = default)
+ {
+ options ??= new CommandEnvironmentOptions();
+
+ return RunCommand(new DotNetCommandOptions
+ {
+ Command = new[] { "--version" },
+ EnvironmentVariables = options.EnvironmentVariables,
+ WorkingDirectory = options.WorkingDirectory,
+ Credentials = options.Credentials,
+ LogInput = options.LogInput,
+ LogOutput = options.LogOutput
+ }, cancellationToken);
+ }
+
public Task CustomCommand(DotNetCommandOptions options, CancellationToken cancellationToken = default)
{
return RunCommand(options, cancellationToken);
@@ -59,36 +91,29 @@ private static DotNetCommandOptions ToDotNetCommandOptions(string command, DotNe
ExtraArguments = options.ExtraArguments,
TargetPath = options.TargetPath,
Configuration = options.Configuration,
- WorkingDirectory = options.WorkingDirectory
+ WorkingDirectory = options.WorkingDirectory,
+ Credentials = options.Credentials,
+ LogInput = options.LogInput,
+ LogOutput = options.LogOutput
};
}
- private Task RunCommand(DotNetCommandOptions options,
- CancellationToken cancellationToken = default)
+ private Task RunCommand(DotNetCommandOptions options, CancellationToken cancellationToken)
{
var arguments = options.Command?.ToList() ?? new List();
- if (options.TargetPath != null)
- {
- arguments.Add(options.TargetPath);
- }
-
- if (options.ExtraArguments != null)
- {
- arguments.AddRange(options.ExtraArguments);
- }
+ arguments.AddNonNullOrEmpty(options.TargetPath);
+ arguments.AddRangeNonNullOrEmpty(options.ExtraArguments);
+ arguments.AddNonNullOrEmptyArgumentWithSwitch("-c", options.Configuration?.ToString());
- if (options.Configuration != null)
- {
- arguments.Add("-c");
- arguments.Add(options.Configuration.ToString()!);
- }
-
- return Context.Command().UsingCommandLineTool(new CommandLineToolOptions("dotnet")
+ return _context.Command.UsingCommandLineTool(new CommandLineToolOptions("dotnet")
{
Arguments = arguments,
EnvironmentVariables = options.EnvironmentVariables,
- WorkingDirectory = options.WorkingDirectory
+ WorkingDirectory = options.WorkingDirectory,
+ Credentials = options.Credentials,
+ LogInput = options.LogInput,
+ LogOutput = options.LogOutput
}, cancellationToken);
}
}
\ No newline at end of file
diff --git a/ModularPipelines.DotNet/DotNetTestResult.cs b/ModularPipelines.DotNet/DotNetTestResult.cs
new file mode 100644
index 0000000000..2fd693926c
--- /dev/null
+++ b/ModularPipelines.DotNet/DotNetTestResult.cs
@@ -0,0 +1,5 @@
+namespace ModularPipelines.DotNet;
+
+public record DotNetTestResult(IReadOnlyList UnitTestResults)
+{
+}
\ No newline at end of file
diff --git a/ModularPipelines.DotNet/Extensions/DotNetExtensions.cs b/ModularPipelines.DotNet/Extensions/DotNetExtensions.cs
index 659b64da68..a7ad5b9ba6 100644
--- a/ModularPipelines.DotNet/Extensions/DotNetExtensions.cs
+++ b/ModularPipelines.DotNet/Extensions/DotNetExtensions.cs
@@ -1,18 +1,28 @@
-using Microsoft.Extensions.DependencyInjection;
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
-using ModularPipelines.Command.Extensions;
using ModularPipelines.Context;
+using ModularPipelines.Engine;
namespace ModularPipelines.DotNet.Extensions;
public static class DotNetExtensions
{
+#pragma warning disable CA2255
+ [ModuleInitializer]
+#pragma warning restore CA2255
+ public static void RegisterDotNetContext()
+ {
+ ServiceContextRegistry.RegisterContext(collection => RegisterDotNetContext(collection));
+ }
+
public static IServiceCollection RegisterDotNetContext(this IServiceCollection services)
{
- services.RegisterCommandContext();
services.TryAddSingleton();
+ services.TryAddSingleton();
return services;
}
-
- public static IDotNet DotNet(this IModuleContext context) => context.Get()!;
+
+ public static IDotNet DotNet(this IModuleContext context) => context.ServiceProvider.GetRequiredService();
+
}
\ No newline at end of file
diff --git a/ModularPipelines.DotNet/IDotNet.cs b/ModularPipelines.DotNet/IDotNet.cs
index 6df7f7684d..c2fbf3c4a4 100644
--- a/ModularPipelines.DotNet/IDotNet.cs
+++ b/ModularPipelines.DotNet/IDotNet.cs
@@ -1,5 +1,6 @@
using CliWrap.Buffered;
using ModularPipelines.DotNet.Options;
+using ModularPipelines.Options;
namespace ModularPipelines.DotNet;
@@ -10,7 +11,9 @@ public interface IDotNet
Task Publish(DotNetOptions options, CancellationToken cancellationToken = default);
Task Pack(DotNetOptions options, CancellationToken cancellationToken = default);
Task Clean(DotNetOptions options, CancellationToken cancellationToken = default);
- Task Test(DotNetOptions options, CancellationToken cancellationToken = default);
+ Task Test(DotNetOptions options, CancellationToken cancellationToken = default);
+
+ Task Version(CommandEnvironmentOptions? options = null, CancellationToken cancellationToken = default);
Task CustomCommand(DotNetCommandOptions options, CancellationToken cancellationToken = default);
}
\ No newline at end of file
diff --git a/ModularPipelines.DotNet/ITrxParser.cs b/ModularPipelines.DotNet/ITrxParser.cs
new file mode 100644
index 0000000000..ea2d9690bb
--- /dev/null
+++ b/ModularPipelines.DotNet/ITrxParser.cs
@@ -0,0 +1,6 @@
+namespace ModularPipelines.DotNet;
+
+public interface ITrxParser
+{
+ DotNetTestResult ParseTestResult(string input);
+}
\ No newline at end of file
diff --git a/ModularPipelines.DotNet/ModularPipelines.DotNet.csproj b/ModularPipelines.DotNet/ModularPipelines.DotNet.csproj
index 29eda92bbe..4decd268f3 100644
--- a/ModularPipelines.DotNet/ModularPipelines.DotNet.csproj
+++ b/ModularPipelines.DotNet/ModularPipelines.DotNet.csproj
@@ -7,7 +7,6 @@
-
diff --git a/ModularPipelines.DotNet/Options/DotNetOptions.cs b/ModularPipelines.DotNet/Options/DotNetOptions.cs
index 9cf7c9a8dd..871462c1ee 100644
--- a/ModularPipelines.DotNet/Options/DotNetOptions.cs
+++ b/ModularPipelines.DotNet/Options/DotNetOptions.cs
@@ -1,14 +1,12 @@
+using ModularPipelines.Options;
+
namespace ModularPipelines.DotNet.Options;
-public record DotNetOptions
+public record DotNetOptions : CommandEnvironmentOptions
{
- public string? WorkingDirectory { get; init; }
-
public string? TargetPath { get; init; }
public IEnumerable? ExtraArguments { get; init; }
public Configuration? Configuration { get; init; }
-
- public IDictionary? EnvironmentVariables { get; init; }
}
\ No newline at end of file
diff --git a/ModularPipelines.DotNet/TestOutput.cs b/ModularPipelines.DotNet/TestOutput.cs
new file mode 100644
index 0000000000..9daf8f4bd2
--- /dev/null
+++ b/ModularPipelines.DotNet/TestOutput.cs
@@ -0,0 +1,10 @@
+using System.Xml.Serialization;
+
+namespace ModularPipelines.DotNet;
+
+[XmlRoot(ElementName = "Output")]
+public record TestOutput
+{
+ [XmlElement("StdOut")]
+ public string? StdOut { get; init; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.DotNet/TrxParser.cs b/ModularPipelines.DotNet/TrxParser.cs
new file mode 100644
index 0000000000..d99ec940e7
--- /dev/null
+++ b/ModularPipelines.DotNet/TrxParser.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using ModularPipelines.Context;
+
+namespace ModularPipelines.DotNet;
+
+public class TrxParser : ITrxParser
+{
+ private readonly IXml _xml;
+
+ public TrxParser(IXml xml)
+ {
+ _xml = xml;
+ }
+
+ public DotNetTestResult ParseTestResult(string input)
+ {
+ return new DotNetTestResult(GetUnitTestResults(input));
+ }
+
+ private List GetUnitTestResults(string input)
+ {
+ return XDocument.Load(new StringReader(input)).Descendants()
+ .Where(d => d.Name.LocalName == "UnitTestResult")
+ .Select(ParseElement)
+ .ToList();
+ }
+
+ private UnitTestResult ParseElement(XElement element)
+ {
+ return new UnitTestResult
+ {
+ ExecutionId = element.Attribute("executionId")!.Value,
+ TestId = element.Attribute("testId")!.Value,
+ TestName = element.Attribute("testName")!.Value,
+ ComputerName = element.Attribute("computerName")!.Value,
+ Duration = element.Attribute("duration")!.Value,
+ StartTime = element.Attribute("startTime")!.Value,
+ EndTime = element.Attribute("endTime")!.Value,
+ TestType = element.Attribute("testType")!.Value,
+ Outcome = element.Attribute("outcome")!.Value,
+ TestListId = element.Attribute("testListId")!.Value,
+ RelativeResultsDirectory = element.Attribute("relativeResultsDirectory")!.Value,
+ Output = new TestOutput
+ {
+ StdOut = element.Descendants().FirstOrDefault(x => x.Name.LocalName == "StdOut")?.Value
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.DotNet/UnitTestResult.cs b/ModularPipelines.DotNet/UnitTestResult.cs
new file mode 100644
index 0000000000..103aed37ca
--- /dev/null
+++ b/ModularPipelines.DotNet/UnitTestResult.cs
@@ -0,0 +1,44 @@
+using System.Xml.Serialization;
+
+namespace ModularPipelines.DotNet;
+
+[Serializable]
+[XmlRoot(ElementName = "UnitTestResult")]
+public record UnitTestResult
+{
+ [XmlAttribute("executionId")]
+ public string ExecutionId { get; init; }
+
+ [XmlAttribute("testId")]
+ public string TestId { get; init; }
+
+ [XmlAttribute("testName")]
+ public string TestName { get; init; }
+
+ [XmlAttribute("computerName")]
+ public string ComputerName { get; init; }
+
+ [XmlAttribute("duration")]
+ public string Duration { get; init; }
+
+ [XmlAttribute("startTime")]
+ public string StartTime { get; init; }
+
+ [XmlAttribute("endTime")]
+ public string EndTime { get; init; }
+
+ [XmlAttribute("testType")]
+ public string TestType { get; init; }
+
+ [XmlAttribute("outcome")]
+ public string Outcome { get; init; }
+
+ [XmlAttribute("testListId")]
+ public string TestListId { get; init; }
+
+ [XmlAttribute("relativeResultsDirectory")]
+ public string RelativeResultsDirectory { get; init; }
+
+ [XmlElement("Output")]
+ public TestOutput Output { get; init; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Examples/ModularPipelines.Examples.csproj b/ModularPipelines.Examples/ModularPipelines.Examples.csproj
index 9c8cbe03d7..f7d8e6e113 100644
--- a/ModularPipelines.Examples/ModularPipelines.Examples.csproj
+++ b/ModularPipelines.Examples/ModularPipelines.Examples.csproj
@@ -5,12 +5,12 @@
net6.0
enable
enable
+ preview
-
@@ -23,4 +23,11 @@
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
diff --git a/ModularPipelines.Examples/Modules/DependentOnSuccessModule.cs b/ModularPipelines.Examples/Modules/DependentOnSuccessModule.cs
new file mode 100644
index 0000000000..b0c5ebc677
--- /dev/null
+++ b/ModularPipelines.Examples/Modules/DependentOnSuccessModule.cs
@@ -0,0 +1,46 @@
+using ModularPipelines.Attributes;
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.Examples.Modules;
+
+[DependsOn]
+public class DependentOnSuccessModule : Module
+{
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
+ return null;
+ }
+}
+
+[DependsOn]
+public class DependentOn2 : Module
+{
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
+ return null;
+ }
+}
+
+[DependsOn]
+public class DependentOn3 : Module
+{
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
+ return null;
+ }
+}
+
+[DependsOn]
+public class DependentOn4 : Module
+{
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Examples/Modules/DotnetTestModule.cs b/ModularPipelines.Examples/Modules/DotnetTestModule.cs
new file mode 100644
index 0000000000..7186c86260
--- /dev/null
+++ b/ModularPipelines.Examples/Modules/DotnetTestModule.cs
@@ -0,0 +1,19 @@
+using ModularPipelines.Context;
+using ModularPipelines.DotNet;
+using ModularPipelines.DotNet.Extensions;
+using ModularPipelines.DotNet.Options;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.Examples.Modules;
+
+public class DotnetTestModule : Module
+{
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ return await context.DotNet().Test(new DotNetOptions
+ {
+ WorkingDirectory = context.Environment.GitRootDirectory!.GetFolder("ModularPipelines.UnitTests").Path
+ }, cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Examples/Modules/FailedModule.cs b/ModularPipelines.Examples/Modules/FailedModule.cs
index 8e8af36260..2f3a032f31 100644
--- a/ModularPipelines.Examples/Modules/FailedModule.cs
+++ b/ModularPipelines.Examples/Modules/FailedModule.cs
@@ -6,13 +6,7 @@ namespace ModularPipelines.Examples.Modules;
public class FailedModule : Module
{
- public FailedModule(IModuleContext context) : base(context)
- {
- }
-
- public override bool IgnoreFailures => true;
-
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken);
diff --git a/ModularPipelines.Examples/Modules/GitLastCommitModule.cs b/ModularPipelines.Examples/Modules/GitLastCommitModule.cs
new file mode 100644
index 0000000000..5a7061a426
--- /dev/null
+++ b/ModularPipelines.Examples/Modules/GitLastCommitModule.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.Logging;
+using ModularPipelines.Context;
+using ModularPipelines.Git.Extensions;
+using ModularPipelines.Git.Models;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.Examples.Modules;
+
+public class GitLastCommitModule : Module
+{
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+
+ context.Logger.LogInformation("Getting Last Git Commit");
+
+ var lastCommit = context.Git().Information.PreviousCommit;
+
+ var allCommits = await context.Git().Information.Commits(cancellationToken: cancellationToken).ToListAsync(cancellationToken: cancellationToken);
+
+ return lastCommit;
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Examples/Modules/GitVersionModule.cs b/ModularPipelines.Examples/Modules/GitVersionModule.cs
index 0345d5f43a..e07dd804b9 100644
--- a/ModularPipelines.Examples/Modules/GitVersionModule.cs
+++ b/ModularPipelines.Examples/Modules/GitVersionModule.cs
@@ -8,12 +8,8 @@ namespace ModularPipelines.Examples.Modules;
public class GitVersionModule : Module
{
- public GitVersionModule(IModuleContext context) : base(context)
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
- }
-
- protected override async Task?> ExecuteAsync(CancellationToken cancellationToken)
- {
- return await Context.Git().Version();
+ return await context.Git().Operations.Version(cancellationToken: cancellationToken);
}
}
\ No newline at end of file
diff --git a/ModularPipelines.Examples/Modules/IgnoredModule.cs b/ModularPipelines.Examples/Modules/IgnoredModule.cs
index fe8dfee7df..3361c46937 100644
--- a/ModularPipelines.Examples/Modules/IgnoredModule.cs
+++ b/ModularPipelines.Examples/Modules/IgnoredModule.cs
@@ -8,11 +8,7 @@ namespace ModularPipelines.Examples.Modules;
[ModuleCategory("Ignore")]
public class IgnoredModule : Module
{
- public IgnoredModule(IModuleContext context) : base(context)
- {
- }
-
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken);
return null;
diff --git a/ModularPipelines.Examples/Modules/NotepadPlusPlusInstallerModule.cs b/ModularPipelines.Examples/Modules/NotepadPlusPlusInstallerModule.cs
index 4868f266d6..1caa41e00b 100644
--- a/ModularPipelines.Examples/Modules/NotepadPlusPlusInstallerModule.cs
+++ b/ModularPipelines.Examples/Modules/NotepadPlusPlusInstallerModule.cs
@@ -1,22 +1,17 @@
using CliWrap.Buffered;
using ModularPipelines.Context;
-using ModularPipelines.Installer.Extensions;
-using ModularPipelines.Installer.Options;
using ModularPipelines.Models;
using ModularPipelines.Modules;
+using ModularPipelines.Options;
namespace ModularPipelines.Examples.Modules;
public class NotepadPlusPlusInstallerModule : Module
{
- public NotepadPlusPlusInstallerModule(IModuleContext context) : base(context)
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
- }
-
- protected override async Task?> ExecuteAsync(CancellationToken cancellationToken)
- {
- return await Context.Installer()
- .InstallFromWeb(new WebInstallerOptions(new Uri(
+ return await context.Installer
+ .InstallFromWebAsync(new WebInstallerOptions(new Uri(
"https://github.com/notepad-plus-plus/notepad-plus-plus/releases/download/v8.5.3/npp.8.5.3.Installer.x64.exe")), cancellationToken);
}
}
\ No newline at end of file
diff --git a/ModularPipelines.Examples/Modules/NugetVersionGeneratorModule.cs b/ModularPipelines.Examples/Modules/NugetVersionGeneratorModule.cs
new file mode 100644
index 0000000000..06fb0f4c79
--- /dev/null
+++ b/ModularPipelines.Examples/Modules/NugetVersionGeneratorModule.cs
@@ -0,0 +1,15 @@
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.Examples.Modules;
+
+public class NugetVersionGeneratorModule : Module
+{
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+
+ return $"{GitVersionInformation.Major}.{GitVersionInformation.Minor}.{GitVersionInformation.Patch}-{GitVersionInformation.PreReleaseLabel}-{GitVersionInformation.CommitsSinceVersionSource}";
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Examples/Modules/SuccessModule.cs b/ModularPipelines.Examples/Modules/SuccessModule.cs
index 93ff133bb5..a843a06f29 100644
--- a/ModularPipelines.Examples/Modules/SuccessModule.cs
+++ b/ModularPipelines.Examples/Modules/SuccessModule.cs
@@ -6,11 +6,7 @@ namespace ModularPipelines.Examples.Modules;
public class SuccessModule : Module
{
- public SuccessModule(IModuleContext context) : base(context)
- {
- }
-
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken);
return null;
diff --git a/ModularPipelines.Examples/Modules/SuccessModule2.cs b/ModularPipelines.Examples/Modules/SuccessModule2.cs
index bb0bec276a..a11e8b812f 100644
--- a/ModularPipelines.Examples/Modules/SuccessModule2.cs
+++ b/ModularPipelines.Examples/Modules/SuccessModule2.cs
@@ -6,11 +6,7 @@ namespace ModularPipelines.Examples.Modules;
public class SuccessModule2 : Module
{
- public SuccessModule2(IModuleContext context) : base(context)
- {
- }
-
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
return null;
diff --git a/ModularPipelines.Examples/Modules/SuccessModule3.cs b/ModularPipelines.Examples/Modules/SuccessModule3.cs
index 8ce9b9705f..9742c22588 100644
--- a/ModularPipelines.Examples/Modules/SuccessModule3.cs
+++ b/ModularPipelines.Examples/Modules/SuccessModule3.cs
@@ -6,11 +6,7 @@ namespace ModularPipelines.Examples.Modules;
public class SuccessModule3 : Module
{
- public SuccessModule3(IModuleContext context) : base(context)
- {
- }
-
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(12), cancellationToken);
diff --git a/ModularPipelines.Examples/Program.cs b/ModularPipelines.Examples/Program.cs
index fcf92eba1f..7a61efdd0e 100644
--- a/ModularPipelines.Examples/Program.cs
+++ b/ModularPipelines.Examples/Program.cs
@@ -2,7 +2,7 @@
using ModularPipelines.Examples.Modules;
using ModularPipelines.Extensions;
using ModularPipelines.Host;
-using ModularPipelines.Requirements;
+using ModularPipelines.Options;
var modules = await PipelineHostBuilder.Create()
.ConfigureAppConfiguration((context, builder) =>
@@ -15,17 +15,26 @@
})
.ConfigurePipelineOptions((context, options) =>
{
+ options.ExecutionMode = ExecutionMode.StopOnFirstException;
options.IgnoreCategories = new[] { "Ignore" };
})
.ConfigureServices((context, collection) =>
{
collection.AddModule()
- .AddModule()
- .AddModule()
- .AddModule()
- .AddModule()
- .AddModule()
- .AddModule()
- .AddRequirement();
+ .AddModule()
+ .AddModule()
+ .AddModule()
+ .AddModule()
+ // .AddModule()
+ // .AddModule()
+ // .AddModule()
+ // .AddModule()
+ // .AddModule()
+ .AddModule()
+ .AddModule();
+ // .AddModule()
+ // .AddModule();
+ //.AddModule()
+ //.AddRequirement();
})
.ExecutePipelineAsync();
\ No newline at end of file
diff --git a/ModularPipelines.Git/Enums/GitStageOption.cs b/ModularPipelines.Git/Enums/GitStageOption.cs
new file mode 100644
index 0000000000..61be5c7388
--- /dev/null
+++ b/ModularPipelines.Git/Enums/GitStageOption.cs
@@ -0,0 +1,8 @@
+namespace ModularPipelines.Git.Enums;
+
+public enum GitStageOption
+{
+ All,
+ CurrentWorkingDirectory,
+ ModifiedOnly
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/Enums/GitStageOptionExtensions.cs b/ModularPipelines.Git/Enums/GitStageOptionExtensions.cs
new file mode 100644
index 0000000000..8f6d59246f
--- /dev/null
+++ b/ModularPipelines.Git/Enums/GitStageOptionExtensions.cs
@@ -0,0 +1,23 @@
+namespace ModularPipelines.Git.Enums;
+
+public static class GitStageOptionExtensions
+{
+ public static string GetCommandLineSwitch(this GitStageOption? option)
+ {
+ if (option is null)
+ {
+ return "-A";
+ }
+
+ return option.Value.GetCommandLineSwitch();
+ }
+
+ public static string GetCommandLineSwitch(this GitStageOption option) =>
+ option switch
+ {
+ GitStageOption.All => "-A",
+ GitStageOption.ModifiedOnly => "-u",
+ GitStageOption.CurrentWorkingDirectory => ".",
+ _ => throw new ArgumentOutOfRangeException(nameof(option))
+ };
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/Extensions/GitExtensions.cs b/ModularPipelines.Git/Extensions/GitExtensions.cs
index 7dea7a5f2a..7204c8f086 100644
--- a/ModularPipelines.Git/Extensions/GitExtensions.cs
+++ b/ModularPipelines.Git/Extensions/GitExtensions.cs
@@ -1,18 +1,31 @@
-using Microsoft.Extensions.DependencyInjection;
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
-using ModularPipelines.Command.Extensions;
using ModularPipelines.Context;
+using ModularPipelines.Engine;
namespace ModularPipelines.Git.Extensions;
public static class GitExtensions
{
+#pragma warning disable CA2255
+ [ModuleInitializer]
+#pragma warning restore CA2255
+ public static void RegisterGitContext()
+ {
+ ServiceContextRegistry.RegisterContext(collection => RegisterGitContext(collection));
+ }
+
public static IServiceCollection RegisterGitContext(this IServiceCollection services)
{
- services.RegisterCommandContext();
services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
return services;
}
-
- public static IGit Git(this IModuleContext context) => context.Get()!;
+
+ public static IGit Git(this IModuleContext context) => context.ServiceProvider.GetRequiredService();
}
\ No newline at end of file
diff --git a/ModularPipelines.Git/Git.cs b/ModularPipelines.Git/Git.cs
index 763dc85e74..1cc47155c7 100644
--- a/ModularPipelines.Git/Git.cs
+++ b/ModularPipelines.Git/Git.cs
@@ -1,76 +1,13 @@
-using CliWrap.Buffered;
-using ModularPipelines.Command.Extensions;
-using ModularPipelines.Command.Options;
-using ModularPipelines.Context;
-using ModularPipelines.Git.Options;
-
namespace ModularPipelines.Git;
public class Git : IGit
{
- public IModuleContext Context { get; }
-
- public Git(IModuleContext context)
- {
- Context = context;
- }
-
- public Task Checkout(GitCheckoutOptions options)
- {
- return Run(options);
- }
-
- public Task Version(GitOptions? options = null)
- {
- var opts = new GitArgumentOptions(new[] {"--version"})
- {
- EnvironmentVariables = options?.EnvironmentVariables,
- WorkingDirectory = options?.WorkingDirectory
- };
-
- return Run(opts);
- }
-
- public Task Fetch(GitOptions? options = null)
+ public Git(IGitOperations operations, IGitInformation information)
{
- var opts = new GitArgumentOptions(new[] {"fetch"})
- {
- EnvironmentVariables = options?.EnvironmentVariables,
- WorkingDirectory = options?.WorkingDirectory
- };
-
- return Run(opts);
- }
-
- public Task Pull(GitOptions? options = null)
- {
- var opts = new GitArgumentOptions(new[] {"pull"})
- {
- EnvironmentVariables = options?.EnvironmentVariables,
- WorkingDirectory = options?.WorkingDirectory
- };
-
- return Run(opts);
+ Operations = operations;
+ Information = information;
}
- public Task Push(GitOptions? options = null)
- {
- var opts = new GitArgumentOptions(new[] {"push"})
- {
- EnvironmentVariables = options?.EnvironmentVariables,
- WorkingDirectory = options?.WorkingDirectory
- };
-
- return Run(opts);
- }
-
- private Task Run(GitArgumentOptions options)
- {
- return Context.Command().UsingCommandLineTool(new CommandLineToolOptions("git")
- {
- Arguments = options.Arguments,
- EnvironmentVariables = options.EnvironmentVariables,
- WorkingDirectory = options.WorkingDirectory
- });
- }
+ public IGitOperations Operations { get; }
+ public IGitInformation Information { get; }
}
\ No newline at end of file
diff --git a/ModularPipelines.Git/GitCommandRunner.cs b/ModularPipelines.Git/GitCommandRunner.cs
new file mode 100644
index 0000000000..eabe143866
--- /dev/null
+++ b/ModularPipelines.Git/GitCommandRunner.cs
@@ -0,0 +1,40 @@
+using ModularPipelines.Context;
+using ModularPipelines.Extensions;
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Git;
+
+internal class GitCommandRunner
+{
+ private readonly IModuleContext _context;
+
+ public GitCommandRunner(IModuleContext context)
+ {
+ _context = context;
+ }
+
+ public async Task RunCommands(CommandEnvironmentOptions? commandEnvironmentOptions, params string?[] commands)
+ {
+ commandEnvironmentOptions ??= new CommandEnvironmentOptions
+ {
+ LogInput = false,
+ LogOutput = false
+ };
+
+ var commandResult = await _context.Command.UsingCommandLineTool(commandEnvironmentOptions.ToCommandLineToolOptions("git", commands.OfType().ToArray()));
+
+ return commandResult.StandardOutput.Trim();
+ }
+
+ public async Task RunCommandsOrNull(CommandEnvironmentOptions? commandEnvironmentOptions, params string?[] commands)
+ {
+ try
+ {
+ return await RunCommands(commandEnvironmentOptions, commands);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/GitCommitMapper.cs b/ModularPipelines.Git/GitCommitMapper.cs
new file mode 100644
index 0000000000..8f9bf9a673
--- /dev/null
+++ b/ModularPipelines.Git/GitCommitMapper.cs
@@ -0,0 +1,37 @@
+using ModularPipelines.Git.Models;
+
+namespace ModularPipelines.Git;
+
+internal class GitCommitMapper : IGitCommitMapper
+{
+ public GitCommit Map(string commandLineOutput)
+ {
+ var lines = commandLineOutput.Split(GitConstants.DotNetLineSeparator, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
+
+ return new GitCommit
+ {
+ Author = new GitAuthor
+ {
+ Name = lines[0],
+ Email = lines[1],
+ Date = DateTime.Parse(lines[2])
+ },
+ Committer = new GitAuthor
+ {
+ Name = lines[3],
+ Email = lines[4],
+ Date = DateTime.Parse(lines[5])
+ },
+ Hash = new GitHash
+ {
+ Long = lines[6],
+ Short = lines[7],
+ },
+ Message = new GitMessage
+ {
+ Subject = lines[8],
+ Body = lines[9]
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/GitConstants.cs b/ModularPipelines.Git/GitConstants.cs
new file mode 100644
index 0000000000..5c2af3f301
--- /dev/null
+++ b/ModularPipelines.Git/GitConstants.cs
@@ -0,0 +1,7 @@
+namespace ModularPipelines.Git;
+
+internal static class GitConstants
+{
+ public const string GitEscapedLineSeparator = "%%%n%%";
+ public const string DotNetLineSeparator = "%\n%";
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/GitInformation.cs b/ModularPipelines.Git/GitInformation.cs
new file mode 100644
index 0000000000..781c2c1af2
--- /dev/null
+++ b/ModularPipelines.Git/GitInformation.cs
@@ -0,0 +1,63 @@
+using System.Runtime.CompilerServices;
+using ModularPipelines.Context;
+using ModularPipelines.Git.Models;
+using ModularPipelines.Git.Options;
+
+namespace ModularPipelines.Git;
+
+internal class GitInformation : IGitInformation
+{
+ private readonly StaticGitInformation _staticGitInformation;
+ private readonly GitCommandRunner _gitCommandRunner;
+ private readonly IGitCommitMapper _gitCommitMapper;
+ private readonly IModuleContext _context;
+
+ public GitInformation(StaticGitInformation staticGitInformation,
+ GitCommandRunner gitCommandRunner,
+ IGitCommitMapper gitCommitMapper,
+ IModuleContext context)
+ {
+ _staticGitInformation = staticGitInformation;
+ _gitCommandRunner = gitCommandRunner;
+ _gitCommitMapper = gitCommitMapper;
+ _context = context;
+ }
+
+ public string BranchName => _staticGitInformation.BranchName;
+ public string DefaultBranchName => _staticGitInformation.DefaultBranchName;
+
+ public string Tag => _staticGitInformation.Tag;
+
+ public GitCommit? PreviousCommit => _staticGitInformation.PreviousCommit;
+
+ public int CommitsOnBranch => _staticGitInformation.CommitsOnBranch;
+ public DateTimeOffset LastCommitDateTime => _staticGitInformation.LastCommitDateTime;
+
+ public string LastCommitSha => _staticGitInformation.LastCommitSha;
+
+ public string LastCommitShortSha => _staticGitInformation.LastCommitShortSha;
+
+
+ public IAsyncEnumerable Commits(GitOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return Commits(null, options, cancellationToken);
+ }
+
+ public async IAsyncEnumerable Commits(string? branch, GitOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var index = 0;
+ while (true)
+ {
+ var output = await _gitCommandRunner.RunCommandsOrNull(options, "log", branch, $"--skip={index-1}", "-1", $"--format='%aN {GitConstants.GitEscapedLineSeparator} %aE {GitConstants.GitEscapedLineSeparator} %aI {GitConstants.GitEscapedLineSeparator} %cN {GitConstants.GitEscapedLineSeparator} %cE {GitConstants.GitEscapedLineSeparator} %cI {GitConstants.GitEscapedLineSeparator} %H {GitConstants.GitEscapedLineSeparator} %h {GitConstants.GitEscapedLineSeparator} %s {GitConstants.GitEscapedLineSeparator} %B'");
+
+ index++;
+
+ if (string.IsNullOrWhiteSpace(output) || cancellationToken.IsCancellationRequested)
+ {
+ yield break;
+ }
+
+ yield return _gitCommitMapper.Map(output);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/GitOperations.cs b/ModularPipelines.Git/GitOperations.cs
new file mode 100644
index 0000000000..f352d1f7b2
--- /dev/null
+++ b/ModularPipelines.Git/GitOperations.cs
@@ -0,0 +1,74 @@
+using CliWrap.Buffered;
+using ModularPipelines.Context;
+using ModularPipelines.Extensions;
+using ModularPipelines.Git.Enums;
+using ModularPipelines.Git.Options;
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Git;
+
+public class GitOperations : IGitOperations
+{
+ private readonly IModuleContext _context;
+
+ public GitOperations(IModuleContext context)
+ {
+ _context = context;
+ }
+
+ public Task Checkout(GitCheckoutOptions options, CancellationToken cancellationToken = default)
+ {
+ return CustomCommand(options, cancellationToken);
+ }
+
+ public Task Version(GitOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return CustomCommand(ToGitCommandOptions(options, new []{"--version"}), cancellationToken);
+ }
+
+ public Task Fetch(GitOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return CustomCommand(ToGitCommandOptions(options, new []{"fetch"}), cancellationToken);
+ }
+
+ public Task Pull(GitOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return CustomCommand(ToGitCommandOptions(options, new []{"pull"}), cancellationToken);
+ }
+
+ public Task Push(GitOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return CustomCommand(ToGitCommandOptions(options, new []{"push"}), cancellationToken);
+ }
+
+ public Task Stage(GitStageOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ var stageOption = options?.GitStageOption ?? GitStageOption.All;
+
+ return CustomCommand(ToGitCommandOptions(options, new []{ "add", stageOption.GetCommandLineSwitch() }), cancellationToken);
+ }
+
+ public Task Commit(string message, GitOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return CustomCommand(ToGitCommandOptions(options, new []{ "commit", "-m", message }), cancellationToken);
+ }
+
+ public Task CustomCommand(GitCommandOptions options, CancellationToken cancellationToken)
+ {
+ return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("git", options.Arguments), cancellationToken);
+ }
+
+ private GitCommandOptions ToGitCommandOptions(CommandEnvironmentOptions? options, IEnumerable arguments)
+ {
+ options ??= new CommandEnvironmentOptions();
+
+ return new GitCommandOptions(arguments)
+ {
+ WorkingDirectory = options.WorkingDirectory,
+ EnvironmentVariables = options.EnvironmentVariables,
+ Credentials = options.Credentials,
+ LogInput = options.LogInput,
+ LogOutput = options.LogOutput
+ };
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/IGit.cs b/ModularPipelines.Git/IGit.cs
index 298b050468..98a65fb161 100644
--- a/ModularPipelines.Git/IGit.cs
+++ b/ModularPipelines.Git/IGit.cs
@@ -1,14 +1,7 @@
-using CliWrap.Buffered;
-using ModularPipelines.Git.Options;
-
-namespace ModularPipelines.Git;
+namespace ModularPipelines.Git;
public interface IGit
{
- Task Checkout(GitCheckoutOptions options);
- Task Version(GitOptions? options = null);
- Task Fetch(GitOptions? options = null);
- Task Pull(GitOptions? options = null);
- Task Push(GitOptions? options = null);
-
+ IGitOperations Operations { get; }
+ IGitInformation Information { get; }
}
\ No newline at end of file
diff --git a/ModularPipelines.Git/IGitCommitMapper.cs b/ModularPipelines.Git/IGitCommitMapper.cs
new file mode 100644
index 0000000000..487b1c62d9
--- /dev/null
+++ b/ModularPipelines.Git/IGitCommitMapper.cs
@@ -0,0 +1,8 @@
+using ModularPipelines.Git.Models;
+
+namespace ModularPipelines.Git;
+
+internal interface IGitCommitMapper
+{
+ GitCommit Map(string commandLineOutput);
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/IGitInformation.cs b/ModularPipelines.Git/IGitInformation.cs
new file mode 100644
index 0000000000..2a73aac60a
--- /dev/null
+++ b/ModularPipelines.Git/IGitInformation.cs
@@ -0,0 +1,19 @@
+using ModularPipelines.Git.Models;
+using ModularPipelines.Git.Options;
+
+namespace ModularPipelines.Git;
+
+public interface IGitInformation
+{
+ public string? BranchName { get; }
+ public string? DefaultBranchName { get; }
+ public string? LastCommitSha { get; }
+ public string? LastCommitShortSha { get; }
+
+ public string? Tag { get; }
+ public int CommitsOnBranch { get; }
+ public DateTimeOffset LastCommitDateTime { get; }
+ public GitCommit? PreviousCommit { get; }
+ IAsyncEnumerable Commits(GitOptions? options = null, CancellationToken cancellationToken = default);
+ IAsyncEnumerable Commits(string branch, GitOptions? options = null, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/IGitOperations.cs b/ModularPipelines.Git/IGitOperations.cs
new file mode 100644
index 0000000000..0d45a0804b
--- /dev/null
+++ b/ModularPipelines.Git/IGitOperations.cs
@@ -0,0 +1,16 @@
+using CliWrap.Buffered;
+using ModularPipelines.Git.Options;
+
+namespace ModularPipelines.Git;
+
+public interface IGitOperations
+{
+ Task Checkout(GitCheckoutOptions options, CancellationToken cancellationToken = default);
+ Task Version(GitOptions? options = null, CancellationToken cancellationToken = default);
+ Task Fetch(GitOptions? options = null, CancellationToken cancellationToken = default);
+ Task Pull(GitOptions? options = null, CancellationToken cancellationToken = default);
+ Task Push(GitOptions? options = null, CancellationToken cancellationToken = default);
+ Task Stage(GitStageOptions? options = null, CancellationToken cancellationToken = default);
+ Task Commit(string message, GitOptions? options = null, CancellationToken cancellationToken = default);
+ Task CustomCommand(GitCommandOptions options, CancellationToken cancellationToken);
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/Models/GitAuthor.cs b/ModularPipelines.Git/Models/GitAuthor.cs
new file mode 100644
index 0000000000..2c55b660ae
--- /dev/null
+++ b/ModularPipelines.Git/Models/GitAuthor.cs
@@ -0,0 +1,8 @@
+namespace ModularPipelines.Git.Models;
+
+public record GitAuthor
+{
+ public string Name { get; set; }
+ public string Email { get; set; }
+ public DateTime Date { get; set; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/Models/GitCommit.cs b/ModularPipelines.Git/Models/GitCommit.cs
new file mode 100644
index 0000000000..807b1924ae
--- /dev/null
+++ b/ModularPipelines.Git/Models/GitCommit.cs
@@ -0,0 +1,9 @@
+namespace ModularPipelines.Git.Models;
+
+public record GitCommit
+{
+ public GitHash Hash { get; set; }
+ public GitAuthor Author { get; set; }
+ public GitAuthor Committer { get; set; }
+ public GitMessage Message { get; set; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/Models/GitFileStatus.cs b/ModularPipelines.Git/Models/GitFileStatus.cs
new file mode 100644
index 0000000000..3e691fc2d5
--- /dev/null
+++ b/ModularPipelines.Git/Models/GitFileStatus.cs
@@ -0,0 +1,7 @@
+namespace ModularPipelines.Git.Models;
+
+public record GitFileStatus
+{
+ public string Status { get; set; }
+ public string File { get; set; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/Models/GitHash.cs b/ModularPipelines.Git/Models/GitHash.cs
new file mode 100644
index 0000000000..880b29ed8f
--- /dev/null
+++ b/ModularPipelines.Git/Models/GitHash.cs
@@ -0,0 +1,7 @@
+namespace ModularPipelines.Git.Models;
+
+public record GitHash
+{
+ public string Long { get; set; }
+ public string Short { get; set; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/Models/GitMessage.cs b/ModularPipelines.Git/Models/GitMessage.cs
new file mode 100644
index 0000000000..7104b50bf2
--- /dev/null
+++ b/ModularPipelines.Git/Models/GitMessage.cs
@@ -0,0 +1,7 @@
+namespace ModularPipelines.Git.Models;
+
+public record GitMessage
+{
+ public string Subject { get; set; }
+ public string Body { get; set; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/ModularPipelines.Git.csproj b/ModularPipelines.Git/ModularPipelines.Git.csproj
index 29eda92bbe..5b7566affd 100644
--- a/ModularPipelines.Git/ModularPipelines.Git.csproj
+++ b/ModularPipelines.Git/ModularPipelines.Git.csproj
@@ -7,8 +7,11 @@
-
+
+
+
+
diff --git a/ModularPipelines.Git/Options/GitArgumentOptions.cs b/ModularPipelines.Git/Options/GitArgumentOptions.cs
deleted file mode 100644
index 1797d2637b..0000000000
--- a/ModularPipelines.Git/Options/GitArgumentOptions.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace ModularPipelines.Git.Options;
-
-public record GitArgumentOptions(IEnumerable Arguments)
-{
- public IDictionary? EnvironmentVariables { get; init; }
- public string? WorkingDirectory { get; init; }
-}
\ No newline at end of file
diff --git a/ModularPipelines.Git/Options/GitCheckoutOptions.cs b/ModularPipelines.Git/Options/GitCheckoutOptions.cs
index 373cccfe3b..1b77c3b6e3 100644
--- a/ModularPipelines.Git/Options/GitCheckoutOptions.cs
+++ b/ModularPipelines.Git/Options/GitCheckoutOptions.cs
@@ -1,3 +1,3 @@
namespace ModularPipelines.Git.Options;
-public record GitCheckoutOptions(string BranchName) : GitArgumentOptions(new [] { "checkout", BranchName });
\ No newline at end of file
+public record GitCheckoutOptions(string BranchName) : GitCommandOptions(new [] { "checkout", BranchName });
\ No newline at end of file
diff --git a/ModularPipelines.Git/Options/GitCommandOptions.cs b/ModularPipelines.Git/Options/GitCommandOptions.cs
new file mode 100644
index 0000000000..89386fc001
--- /dev/null
+++ b/ModularPipelines.Git/Options/GitCommandOptions.cs
@@ -0,0 +1,7 @@
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Git.Options;
+
+public record GitCommandOptions(IEnumerable Arguments) : CommandEnvironmentOptions
+{
+}
\ No newline at end of file
diff --git a/ModularPipelines.Git/Options/GitOptions.cs b/ModularPipelines.Git/Options/GitOptions.cs
index ab17cfb529..52045423ff 100644
--- a/ModularPipelines.Git/Options/GitOptions.cs
+++ b/ModularPipelines.Git/Options/GitOptions.cs
@@ -1,7 +1,5 @@
-namespace ModularPipelines.Git.Options;
+using ModularPipelines.Options;
-public record GitOptions
-{
- public IDictionary? EnvironmentVariables { get; init; }
- public string? WorkingDirectory { get; init; }
-}
\ No newline at end of file
+namespace ModularPipelines.Git.Options;
+
+public record GitOptions : CommandEnvironmentOptions;
\ No newline at end of file
diff --git a/ModularPipelines.Git/Options/GitStageOptions.cs b/ModularPipelines.Git/Options/GitStageOptions.cs
new file mode 100644
index 0000000000..9721401502
--- /dev/null
+++ b/ModularPipelines.Git/Options/GitStageOptions.cs
@@ -0,0 +1,6 @@
+using ModularPipelines.Git.Enums;
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Git.Options;
+
+public record GitStageOptions(GitStageOption GitStageOption) : CommandEnvironmentOptions;
\ No newline at end of file
diff --git a/ModularPipelines.Git/StaticGitInformation.cs b/ModularPipelines.Git/StaticGitInformation.cs
new file mode 100644
index 0000000000..26b8dd4b80
--- /dev/null
+++ b/ModularPipelines.Git/StaticGitInformation.cs
@@ -0,0 +1,78 @@
+using System.Runtime.CompilerServices;
+using ModularPipelines.Git.Models;
+using ModularPipelines.Git.Options;
+using TomLonghurst.Microsoft.Extensions.DependencyInjection.ServiceInitialization;
+
+namespace ModularPipelines.Git;
+
+internal class StaticGitInformation : IGitInformation, IInitializer
+{
+ private readonly GitCommandRunner _gitCommandRunner;
+ private readonly IGitCommitMapper _gitCommitMapper;
+
+ public StaticGitInformation(GitCommandRunner gitCommandRunner, IGitCommitMapper gitCommitMapper)
+ {
+ _gitCommandRunner = gitCommandRunner;
+ _gitCommitMapper = gitCommitMapper;
+ }
+
+ public async Task InitializeAsync()
+ {
+ try
+ {
+ await _gitCommandRunner.RunCommands(null, "version");
+ }
+ catch (Exception e)
+ {
+ throw new Exception("Error detecting Git repository", e);
+ }
+
+ BranchName = await _gitCommandRunner.RunCommandsOrNull(null, "rev-parse", "--abbrev-ref", "HEAD");
+ DefaultBranchName = (await _gitCommandRunner.RunCommandsOrNull(null, "rev-parse", "--abbrev-ref", "origin/HEAD"))?.Replace("origin/", string.Empty);
+ LastCommitSha = await _gitCommandRunner.RunCommandsOrNull(null, "rev-parse", "HEAD");
+ LastCommitShortSha = await _gitCommandRunner.RunCommandsOrNull(null, "rev-parse", "--short", "HEAD");
+ Tag = await _gitCommandRunner.RunCommandsOrNull(null, "describe", "--tags");
+ CommitsOnBranch = int.Parse(await _gitCommandRunner.RunCommandsOrNull(null, "rev-list", "HEAD", "--count") ?? "0");
+ LastCommitDateTime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(await _gitCommandRunner.RunCommandsOrNull(null, "log", "-1", "--format=%at") ?? "0"));
+ PreviousCommit = await LastCommits(1).FirstOrDefaultAsync();
+ }
+
+ public async IAsyncEnumerable LastCommits(int count, GitOptions? gitOptions = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ for (var i = 0; i < count; i++)
+ {
+ var output = await _gitCommandRunner.RunCommandsOrNull(gitOptions, "log", $"--skip={i-1}", "-1", $"--format='%aN {GitConstants.GitEscapedLineSeparator} %aE {GitConstants.GitEscapedLineSeparator} %aI {GitConstants.GitEscapedLineSeparator} %cN {GitConstants.GitEscapedLineSeparator} %cE {GitConstants.GitEscapedLineSeparator} %cI {GitConstants.GitEscapedLineSeparator} %H {GitConstants.GitEscapedLineSeparator} %h {GitConstants.GitEscapedLineSeparator} %s {GitConstants.GitEscapedLineSeparator} %b'");
+
+ if (string.IsNullOrWhiteSpace(output) || cancellationToken.IsCancellationRequested)
+ {
+ yield break;
+ }
+
+ yield return _gitCommitMapper.Map(output);
+ }
+ }
+
+ public GitCommit? PreviousCommit { get; private set; }
+
+ public string? BranchName { get; private set; } = null!;
+ public string? DefaultBranchName { get; private set; } = null!;
+
+ public string? Tag { get; private set; } = null!;
+
+ public int CommitsOnBranch { get; private set; }
+ public DateTimeOffset LastCommitDateTime { get; private set; }
+
+ public string? LastCommitSha { get; private set; } = null!;
+
+ public string? LastCommitShortSha { get; private set; } = null!;
+
+ public IAsyncEnumerable Commits(GitOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IAsyncEnumerable Commits(string branch, GitOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Installer/Extensions/InstallerExtensions.cs b/ModularPipelines.Installer/Extensions/InstallerExtensions.cs
deleted file mode 100644
index bc0797fa98..0000000000
--- a/ModularPipelines.Installer/Extensions/InstallerExtensions.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
-using ModularPipelines.Command.Extensions;
-using ModularPipelines.Context;
-
-namespace ModularPipelines.Installer.Extensions;
-
-public static class InstallerExtensions
-{
- public static IServiceCollection RegisterInstallerContext(this IServiceCollection services)
- {
- services.RegisterCommandContext();
-
- services.TryAddSingleton();
- return services;
- }
-
- public static IInstaller Installer(this IModuleContext context) => context.Get()!;
-}
\ No newline at end of file
diff --git a/ModularPipelines.Installer/IInstaller.cs b/ModularPipelines.Installer/IInstaller.cs
deleted file mode 100644
index 8b7fb234b9..0000000000
--- a/ModularPipelines.Installer/IInstaller.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using CliWrap.Buffered;
-using ModularPipelines.Installer.Options;
-
-namespace ModularPipelines.Installer;
-
-public interface IInstaller
-{
- Task InstallFromFile(InstallerOptions options, CancellationToken cancellationToken = default);
- Task InstallFromWeb(WebInstallerOptions options,
- CancellationToken cancellationToken = default);
-}
\ No newline at end of file
diff --git a/ModularPipelines.Installer/Installer.cs b/ModularPipelines.Installer/Installer.cs
deleted file mode 100644
index cf8b7bf79d..0000000000
--- a/ModularPipelines.Installer/Installer.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using CliWrap.Buffered;
-using ModularPipelines.Command.Extensions;
-using ModularPipelines.Command.Options;
-using ModularPipelines.Context;
-using ModularPipelines.Installer.Options;
-
-namespace ModularPipelines.Installer;
-
-public class Installer : IInstaller
-{
- public IModuleContext Context { get; }
-
- public Installer(IModuleContext context)
- {
- Context = context;
- }
-
- public Task InstallFromFile(InstallerOptions options,
- CancellationToken cancellationToken = default)
- {
- return Context.Command().UsingCommandLineTool(new CommandLineToolOptions(options.Path)
- {
- Arguments = options.Arguments ?? Array.Empty()
- }, cancellationToken);
- }
-
- public async Task InstallFromWeb(WebInstallerOptions options,
- CancellationToken cancellationToken = default)
- {
- var httpClient = Context.Get()!;
-
- await using var stream = await httpClient.GetStreamAsync(options.DownloadUri, cancellationToken);
-
- var filePathToSave = Path.GetTempFileName() + Guid.NewGuid() + GetExtension(options.DownloadUri.AbsoluteUri);
-
- await using (var newFile = File.Create(filePathToSave))
- {
- await stream.CopyToAsync(newFile, cancellationToken);
- }
-
- return await InstallFromFile(new InstallerOptions(filePathToSave)
- {
- Arguments = options.Arguments
- }, cancellationToken);
- }
-
- private string GetExtension(string downloadUri)
- {
- if (Path.HasExtension(downloadUri))
- {
- return Path.GetExtension(downloadUri);
- }
-
- return string.Empty;
- }
-}
\ No newline at end of file
diff --git a/ModularPipelines.MicrosoftTeams/Extensions/MicrosoftTeamsExtensions.cs b/ModularPipelines.MicrosoftTeams/Extensions/MicrosoftTeamsExtensions.cs
index de656ac6fa..cc6fc585c5 100644
--- a/ModularPipelines.MicrosoftTeams/Extensions/MicrosoftTeamsExtensions.cs
+++ b/ModularPipelines.MicrosoftTeams/Extensions/MicrosoftTeamsExtensions.cs
@@ -1,16 +1,26 @@
-using Microsoft.Extensions.DependencyInjection;
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using ModularPipelines.Context;
+using ModularPipelines.Engine;
namespace ModularPipelines.MicrosoftTeams.Extensions;
public static class MicrosoftTeamsExtensions
{
+#pragma warning disable CA2255
+ [ModuleInitializer]
+#pragma warning restore CA2255
+ public static void RegisterMicrosoftTeamsContext()
+ {
+ ServiceContextRegistry.RegisterContext(collection => RegisterMicrosoftTeamsContext(collection));
+ }
+
public static IServiceCollection RegisterMicrosoftTeamsContext(this IServiceCollection services)
{
services.TryAddSingleton();
return services;
}
-
- public static IMicrosoftTeams MicrosoftTeams(this IModuleContext context) => context.Get()!;
+
+ public static IMicrosoftTeams MicrosoftTeams(this IModuleContext context) => context.ServiceProvider.GetRequiredService();
}
\ No newline at end of file
diff --git a/ModularPipelines.Node/Extensions/NodeExtensions.cs b/ModularPipelines.Node/Extensions/NodeExtensions.cs
new file mode 100644
index 0000000000..395c5c0fab
--- /dev/null
+++ b/ModularPipelines.Node/Extensions/NodeExtensions.cs
@@ -0,0 +1,28 @@
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using ModularPipelines.Context;
+using ModularPipelines.Engine;
+
+namespace ModularPipelines.Node.Extensions;
+
+public static class NodeExtensions
+{
+#pragma warning disable CA2255
+ [ModuleInitializer]
+#pragma warning restore CA2255
+ public static void RegisterNodeContext()
+ {
+ ServiceContextRegistry.RegisterContext(collection => RegisterNodeContext(collection));
+ }
+
+ public static IServiceCollection RegisterNodeContext(this IServiceCollection services)
+ {
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ return services;
+ }
+
+ public static INode Node(this IModuleContext context) => context.ServiceProvider.GetRequiredService();
+}
\ No newline at end of file
diff --git a/ModularPipelines.Node/INode.cs b/ModularPipelines.Node/INode.cs
new file mode 100644
index 0000000000..a8df755a17
--- /dev/null
+++ b/ModularPipelines.Node/INode.cs
@@ -0,0 +1,10 @@
+using CliWrap.Buffered;
+
+namespace ModularPipelines.Node;
+
+public interface INode
+{
+ Task Version(CancellationToken cancellationToken = default);
+ public INpm Npm { get; }
+ public INvm Nvm { get; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Node/INpm.cs b/ModularPipelines.Node/INpm.cs
new file mode 100644
index 0000000000..ecf72dfdda
--- /dev/null
+++ b/ModularPipelines.Node/INpm.cs
@@ -0,0 +1,11 @@
+using CliWrap.Buffered;
+using ModularPipelines.Node.Models;
+
+namespace ModularPipelines.Node;
+
+public interface INpm
+{
+ Task Install(NpmInstallOptions options, CancellationToken cancellationToken = default);
+ Task CleanInstall(NpmCleanInstallOptions options, CancellationToken cancellationToken = default);
+ Task Run(NpmRunOptions options, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/ModularPipelines.Node/INvm.cs b/ModularPipelines.Node/INvm.cs
new file mode 100644
index 0000000000..14fdd175a4
--- /dev/null
+++ b/ModularPipelines.Node/INvm.cs
@@ -0,0 +1,8 @@
+using CliWrap.Buffered;
+
+namespace ModularPipelines.Node;
+
+public interface INvm
+{
+ Task Use(string version, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/ModularPipelines.Node/Models/NpmCleanInstallOptions.cs b/ModularPipelines.Node/Models/NpmCleanInstallOptions.cs
new file mode 100644
index 0000000000..b463a27119
--- /dev/null
+++ b/ModularPipelines.Node/Models/NpmCleanInstallOptions.cs
@@ -0,0 +1,9 @@
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Node.Models;
+
+public record NpmCleanInstallOptions : CommandEnvironmentOptions
+{
+ public string? InstallStrategy { get; init; }
+ public IEnumerable? Omit { get; init; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Node/Models/NpmInstallOptions.cs b/ModularPipelines.Node/Models/NpmInstallOptions.cs
new file mode 100644
index 0000000000..648d8c69a4
--- /dev/null
+++ b/ModularPipelines.Node/Models/NpmInstallOptions.cs
@@ -0,0 +1,11 @@
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Node.Models;
+
+public record NpmInstallOptions : CommandEnvironmentOptions
+{
+ public string? Target { get; init; }
+ public bool Global { get; init; }
+ public bool DryRun { get; init; }
+ public bool Force { get; init; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Node/Models/NpmRunOptions.cs b/ModularPipelines.Node/Models/NpmRunOptions.cs
new file mode 100644
index 0000000000..b3c948a951
--- /dev/null
+++ b/ModularPipelines.Node/Models/NpmRunOptions.cs
@@ -0,0 +1,9 @@
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Node.Models;
+
+public record NpmRunOptions : CommandEnvironmentOptions
+{
+ public string Target { get; init; }
+ public IEnumerable? Arguments { get; init; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Node/ModularPipelines.Node.csproj b/ModularPipelines.Node/ModularPipelines.Node.csproj
new file mode 100644
index 0000000000..4decd268f3
--- /dev/null
+++ b/ModularPipelines.Node/ModularPipelines.Node.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/ModularPipelines.Node/Node.cs b/ModularPipelines.Node/Node.cs
new file mode 100644
index 0000000000..0dbaf13160
--- /dev/null
+++ b/ModularPipelines.Node/Node.cs
@@ -0,0 +1,27 @@
+using CliWrap.Buffered;
+using ModularPipelines.Context;
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Node;
+
+public class Node : INode
+{
+ private readonly IModuleContext _context;
+ public INpm Npm { get; }
+ public INvm Nvm { get; }
+
+ public Node(INpm npm, INvm nvm, IModuleContext context)
+ {
+ _context = context;
+ Npm = npm;
+ Nvm = nvm;
+ }
+
+ public Task Version(CancellationToken cancellationToken = default)
+ {
+ return _context.Command.UsingCommandLineTool(new CommandLineToolOptions("node")
+ {
+ Arguments = new []{ "-v" }
+ }, cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Node/Npm.cs b/ModularPipelines.Node/Npm.cs
new file mode 100644
index 0000000000..a683404aa2
--- /dev/null
+++ b/ModularPipelines.Node/Npm.cs
@@ -0,0 +1,65 @@
+using CliWrap.Buffered;
+using ModularPipelines.Context;
+using ModularPipelines.Extensions;
+using ModularPipelines.Node.Models;
+
+namespace ModularPipelines.Node;
+
+public class Npm : INpm
+{
+ private readonly IModuleContext _context;
+
+ public Npm(IModuleContext context)
+ {
+ _context = context;
+ }
+
+ public Task Install(NpmInstallOptions options, CancellationToken cancellationToken = default)
+ {
+ var arguments = new List { "install" };
+
+ arguments.AddNonNullOrEmpty(options.Target);
+
+ if (options.Global)
+ {
+ arguments.Add("-g");
+ }
+
+ if (options.Force)
+ {
+ arguments.Add("--force");
+ }
+
+ if (options.DryRun)
+ {
+ arguments.Add("--dry-run");
+ }
+
+ return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("npm", arguments), cancellationToken);
+ }
+
+ public Task CleanInstall(NpmCleanInstallOptions options, CancellationToken cancellationToken = default)
+ {
+ var arguments = new List { "ci" };
+
+ arguments.AddNonNullOrEmptyArgumentWithPrefix("--install-strategy=", options.InstallStrategy);
+ arguments.AddRangeNonNullOrEmptyArgumentWithPrefix("--omit=", options.Omit);
+
+ return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("npm", arguments), cancellationToken);
+ }
+
+ public Task Run(NpmRunOptions options, CancellationToken cancellationToken = default)
+ {
+ var arguments = new List { "run" };
+
+ arguments.AddNonNullOrEmpty(options.Target);
+
+ if (options.Arguments?.Any() == true)
+ {
+ arguments.Add("--");
+ arguments.AddRange(options.Arguments);
+ }
+
+ return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("npm", arguments), cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Node/Nvm.cs b/ModularPipelines.Node/Nvm.cs
new file mode 100644
index 0000000000..197d3f62cf
--- /dev/null
+++ b/ModularPipelines.Node/Nvm.cs
@@ -0,0 +1,23 @@
+using CliWrap.Buffered;
+using ModularPipelines.Context;
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Node;
+
+public class Nvm : INvm
+{
+ private readonly IModuleContext _context;
+
+ public Nvm(IModuleContext context)
+ {
+ _context = context;
+ }
+
+ public Task Use(string version, CancellationToken cancellationToken = default)
+ {
+ return _context.Command.UsingCommandLineTool(new CommandLineToolOptions("nvm")
+ {
+ Arguments = new []{ "use", version }
+ }, cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.NuGet/Extensions/NuGetExtensions.cs b/ModularPipelines.NuGet/Extensions/NuGetExtensions.cs
index 81c7e57690..523d1ebe14 100644
--- a/ModularPipelines.NuGet/Extensions/NuGetExtensions.cs
+++ b/ModularPipelines.NuGet/Extensions/NuGetExtensions.cs
@@ -1,21 +1,30 @@
-using Microsoft.Extensions.DependencyInjection;
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
-using ModularPipelines.Command.Extensions;
using ModularPipelines.Context;
using ModularPipelines.DotNet.Extensions;
+using ModularPipelines.Engine;
namespace ModularPipelines.NuGet.Extensions;
public static class NuGetExtensions
{
+#pragma warning disable CA2255
+ [ModuleInitializer]
+#pragma warning restore CA2255
+ public static void RegisterNuGetContext()
+ {
+ ServiceContextRegistry.RegisterContext(collection => RegisterNuGetContext(collection));
+ }
+
public static IServiceCollection RegisterNuGetContext(this IServiceCollection services)
{
- services.RegisterCommandContext()
- .RegisterDotNetContext();
-
+ services.RegisterDotNetContext();
+
services.TryAddSingleton();
+
return services;
}
-
- public static INuGet NuGet(this IModuleContext context) => context.Get()!;
+
+ public static INuGet NuGet(this IModuleContext context) => (INuGet) context.ServiceProvider.GetRequiredService();
}
\ No newline at end of file
diff --git a/ModularPipelines.NuGet/INuGet.cs b/ModularPipelines.NuGet/INuGet.cs
index 4fdd5083fd..c50819149c 100644
--- a/ModularPipelines.NuGet/INuGet.cs
+++ b/ModularPipelines.NuGet/INuGet.cs
@@ -5,7 +5,7 @@ namespace ModularPipelines.NuGet;
public interface INuGet
{
- Task> UploadPackage(NuGetUploadOptions options);
+ Task> UploadPackages(NuGetUploadOptions options);
Task AddSource(NuGetSourceOptions options);
}
\ No newline at end of file
diff --git a/ModularPipelines.NuGet/NuGet.cs b/ModularPipelines.NuGet/NuGet.cs
index c63fa682b3..cb04b8f79e 100644
--- a/ModularPipelines.NuGet/NuGet.cs
+++ b/ModularPipelines.NuGet/NuGet.cs
@@ -1,38 +1,39 @@
using CliWrap.Buffered;
-using ModularPipelines.Command.Extensions;
-using ModularPipelines.Command.Options;
using ModularPipelines.Context;
using ModularPipelines.DotNet.Extensions;
using ModularPipelines.DotNet.Options;
+using ModularPipelines.Extensions;
using ModularPipelines.NuGet.Options;
+using ModularPipelines.Options;
namespace ModularPipelines.NuGet;
public class NuGet : INuGet
{
- public IModuleContext Context { get; }
+ private readonly IModuleContext _context;
public NuGet(IModuleContext context)
{
- Context = context;
+ _context = context;
}
- public async Task> UploadPackage(NuGetUploadOptions options)
+ public async Task> UploadPackages(NuGetUploadOptions options)
{
var results = new List();
foreach (var packagePath in options.PackagePaths)
{
- var commandResult = await Context.Command()
+ var arguments = new List
+ {
+ "nuget", "push", packagePath, "-n"
+ };
+
+ arguments.AddNonNullOrEmptyArgumentWithSwitch("-s", options.FeedUri.AbsoluteUri);
+ arguments.AddNonNullOrEmptyArgumentWithSwitch("-k", options.ApiKey);
+
+ var commandResult = await _context.Command
.UsingCommandLineTool(new CommandLineToolOptions("dotnet")
{
- Arguments = new []
- {
- "nuget", "push",
- packagePath,
- "-s", options.FeedUri.AbsoluteUri,
- "-k", options.ApiKey ?? string.Empty,
- "-n"
- }
+ Arguments = arguments
});
results.Add(commandResult);
@@ -49,21 +50,17 @@ public Task AddSource(NuGetSourceOptions options)
"-n", options.Name
};
- if (options.Username != null)
- {
- arguments.Add("--username");
- arguments.Add(options.Username);
- }
-
- if (options.Password != null)
- {
- arguments.Add("--password");
- arguments.Add(options.Password);
- }
-
- return Context.DotNet().CustomCommand(new DotNetCommandOptions
+ arguments.AddNonNullOrEmptyArgumentWithSwitch("--username", options.Username);
+ arguments.AddNonNullOrEmptyArgumentWithSwitch("--password", options.Password);
+
+ return _context.DotNet().CustomCommand(new DotNetCommandOptions
{
- Command = arguments
+ Command = arguments,
+ EnvironmentVariables = options.EnvironmentVariables,
+ WorkingDirectory = options.WorkingDirectory,
+ Credentials = options.Credentials,
+ LogInput = options.LogInput,
+ LogOutput = options.LogOutput
});
}
}
\ No newline at end of file
diff --git a/ModularPipelines.NuGet/Options/NuGetSourceOptions.cs b/ModularPipelines.NuGet/Options/NuGetSourceOptions.cs
index 575d12228d..864817a4ae 100644
--- a/ModularPipelines.NuGet/Options/NuGetSourceOptions.cs
+++ b/ModularPipelines.NuGet/Options/NuGetSourceOptions.cs
@@ -1,10 +1,12 @@
-namespace ModularPipelines.NuGet.Options;
+using ModularPipelines.Options;
+
+namespace ModularPipelines.NuGet.Options;
public record NuGetSourceOptions
(
Uri FeedUri,
string Name
-)
+) : CommandEnvironmentOptions
{
public string? Username { get; init; }
public string? Password { get; init; }
diff --git a/ModularPipelines.Powershell/Extensions/PowershellExtensions.cs b/ModularPipelines.Powershell/Extensions/PowershellExtensions.cs
new file mode 100644
index 0000000000..92b3493f9e
--- /dev/null
+++ b/ModularPipelines.Powershell/Extensions/PowershellExtensions.cs
@@ -0,0 +1,27 @@
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using ModularPipelines.Context;
+using ModularPipelines.Engine;
+
+namespace ModularPipelines.Powershell.Extensions;
+
+public static class PowershellExtensions
+{
+#pragma warning disable CA2255
+ [ModuleInitializer]
+#pragma warning restore CA2255
+ public static void RegisterPowershellContext()
+ {
+ ServiceContextRegistry.RegisterContext(collection => RegisterPowershellContext(collection));
+ }
+
+ public static IServiceCollection RegisterPowershellContext(this IServiceCollection services)
+ {
+ services.TryAddSingleton();
+
+ return services;
+ }
+
+ public static IPowershell Powershell(this IModuleContext context) => context.ServiceProvider.GetRequiredService();
+}
\ No newline at end of file
diff --git a/ModularPipelines.Powershell/IPowershell.cs b/ModularPipelines.Powershell/IPowershell.cs
new file mode 100644
index 0000000000..8699ea80e4
--- /dev/null
+++ b/ModularPipelines.Powershell/IPowershell.cs
@@ -0,0 +1,10 @@
+using CliWrap.Buffered;
+using ModularPipelines.Powershell.Models;
+
+namespace ModularPipelines.Powershell;
+
+public interface IPowershell
+{
+ Task Script(PowershellScriptOptions options, CancellationToken cancellationToken = default);
+ Task FromFile(PowershellFileOptions options, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/ModularPipelines.Powershell/Models/PowershellFileOptions.cs b/ModularPipelines.Powershell/Models/PowershellFileOptions.cs
new file mode 100644
index 0000000000..8411b24509
--- /dev/null
+++ b/ModularPipelines.Powershell/Models/PowershellFileOptions.cs
@@ -0,0 +1,8 @@
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Powershell.Models;
+
+public record PowershellFileOptions(string FilePath) : CommandEnvironmentOptions
+{
+ public IEnumerable? Arguments { get; init; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Powershell/Models/PowershellScriptOptions.cs b/ModularPipelines.Powershell/Models/PowershellScriptOptions.cs
new file mode 100644
index 0000000000..4e531d1f1e
--- /dev/null
+++ b/ModularPipelines.Powershell/Models/PowershellScriptOptions.cs
@@ -0,0 +1,8 @@
+using ModularPipelines.Options;
+
+namespace ModularPipelines.Powershell.Models;
+
+public record PowershellScriptOptions(string Script) : CommandEnvironmentOptions
+{
+ public IEnumerable? Arguments { get; init; }
+}
\ No newline at end of file
diff --git a/ModularPipelines.Powershell/ModularPipelines.Powershell.csproj b/ModularPipelines.Powershell/ModularPipelines.Powershell.csproj
new file mode 100644
index 0000000000..4decd268f3
--- /dev/null
+++ b/ModularPipelines.Powershell/ModularPipelines.Powershell.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/ModularPipelines.Powershell/Powershell.cs b/ModularPipelines.Powershell/Powershell.cs
new file mode 100644
index 0000000000..b7d86669bb
--- /dev/null
+++ b/ModularPipelines.Powershell/Powershell.cs
@@ -0,0 +1,34 @@
+using CliWrap.Buffered;
+using ModularPipelines.Context;
+using ModularPipelines.Extensions;
+using ModularPipelines.Powershell.Models;
+
+namespace ModularPipelines.Powershell;
+
+public class Powershell : IPowershell
+{
+ private readonly IModuleContext _context;
+
+ public Powershell(IModuleContext context)
+ {
+ _context = context;
+ }
+
+ public Task Script(PowershellScriptOptions options, CancellationToken cancellationToken = default)
+ {
+ var arguments = new List { "-Command", options.Script };
+
+ arguments.AddRangeNonNullOrEmpty(options.Arguments);
+
+ return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("pwsh", arguments), cancellationToken);
+ }
+
+ public Task FromFile(PowershellFileOptions options, CancellationToken cancellationToken = default)
+ {
+ var arguments = new List { "-File", options.FilePath };
+
+ arguments.AddRangeNonNullOrEmpty(options.Arguments);
+
+ return _context.Command.UsingCommandLineTool(options.ToCommandLineToolOptions("pwsh", arguments), cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/AsyncDisposableModuleTests.cs b/ModularPipelines.UnitTests/AsyncDisposableModuleTests.cs
index aa9397b1fe..12c471137d 100644
--- a/ModularPipelines.UnitTests/AsyncDisposableModuleTests.cs
+++ b/ModularPipelines.UnitTests/AsyncDisposableModuleTests.cs
@@ -24,11 +24,8 @@ public async Task SuccessfullyDisposed()
public class AsyncDisposableModule : Module, IAsyncDisposable
{
public bool IsDisposed { get; private set; }
- public AsyncDisposableModule(IModuleContext context) : base(context)
- {
- }
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
await Task.Delay(100, cancellationToken);
return null;
diff --git a/ModularPipelines.UnitTests/Data/Lorem.txt b/ModularPipelines.UnitTests/Data/Lorem.txt
new file mode 100644
index 0000000000..27b1c3b6df
--- /dev/null
+++ b/ModularPipelines.UnitTests/Data/Lorem.txt
@@ -0,0 +1,49 @@
+Vel odio ipsum amet ea clita quis ipsum sed sed erat. Sit elitr delenit justo in ut dolor dolor option tincidunt at lorem sea aliquyam duo wisi est. Lorem vulputate magna. Dolor nihil quis ipsum tincidunt duo. Ut et eu nonumy consetetur ea tincidunt eirmod sed blandit rebum aliquyam voluptua sit vero dolor aliquam eirmod. Tempor lorem quod aliquyam invidunt esse eum eos erat dolores stet consetetur diam aliquyam vero facilisis nulla sit et. Ea voluptua nibh diam magna. Eros te dolore diam amet tempor gubergren hendrerit nostrud justo sea tempor mazim facer feugiat sadipscing rebum. Et exerci invidunt. Accusam duo amet sit ea diam. Dolores vulputate ut et eum stet dolor diam dolore. At duo ipsum esse lorem velit est et commodo. Odio sit tempor kasd takimata diam at ut zzril dolore amet et tempor lorem elitr ipsum. Hendrerit clita diam nibh molestie at est. Velit aliquip dolor dolores zzril sed duis est eirmod duo ipsum tempor suscipit sadipscing facilisis eirmod nostrud nostrud.
+
+Est feugiat amet aliquyam takimata facilisi amet tation et facer labore labore et. Dolor invidunt sed. Sit sit assum et ipsum invidunt sit sanctus takimata dolor lorem in dolores erat at.
+
+Vero dolor sit gubergren sea amet invidunt labore dolores et sit tempor dolor takimata vulputate. Est nostrud lorem aliquyam vulputate blandit stet nibh sed et tempor at commodo ut sanctus. Sit ut sanctus wisi diam et elitr amet nisl magna delenit ipsum clita vero accumsan nonumy delenit.
+
+Molestie est takimata sed invidunt eos sadipscing dolore feugait. Facilisis dignissim consetetur diam eirmod feugait ea erat accusam. Voluptua volutpat dolor sit sea justo no sed eos nulla dolor eros dolor. Tempor elitr sanctus diam. Kasd ut rebum. Placerat velit clita et vero dolore rebum sea gubergren hendrerit rebum lobortis accusam. Amet dignissim est dolore ipsum in dolores aliquyam tempor velit. Eos sit lobortis et elitr gubergren rebum kasd et ipsum nam dolor lorem diam at exerci lorem diam lorem. Takimata et gubergren consetetur assum nobis commodo amet est sit kasd consequat in. Lorem dignissim erat et duo enim. Quod sanctus ipsum. Velit sanctus adipiscing diam ipsum duo at labore lorem dolore sed ut consetetur et no. Consectetuer tincidunt eleifend et et nonumy rebum sadipscing in. Praesent tempor dolores eirmod stet diam takimata tempor rebum erat stet eos dolor volutpat esse lorem id. Clita vel ut kasd elitr tempor velit et eos eu diam sit consetetur takimata tation diam. Velit accusam nibh augue delenit sed vero sed magna et gubergren eos labore ipsum.
+
+Invidunt magna qui consetetur est sed magna dolor. Erat consetetur ut lorem dolor. Ea stet dolore duis erat ea ad dolore facilisi clita assum duis tempor et veniam diam rebum diam accusam. At ipsum amet sed eu diam hendrerit justo diam praesent odio sadipscing sed diam sed kasd sit in amet. Facer dolore clita et invidunt luptatum. Dolor rebum kasd magna. Diam amet magna. Dolores erat sit nonumy eos dolor et nonumy takimata amet illum gubergren dolor diam possim nisl stet no. Sadipscing in stet tempor stet stet et ipsum iriure erat illum amet.
+
+Et eum rebum at takimata laoreet sit doming eos. Sed ipsum velit vel no. Sadipscing consectetuer ea tincidunt eirmod lorem est. Tempor elitr diam est gubergren diam ullamcorper iriure lorem erat ea amet voluptua. Magna sit possim ut qui est velit et iriure nisl amet justo sanctus lorem ea. Eum clita esse justo tempor ea adipiscing diam ut qui et aliquyam ipsum nisl dolor ea justo.
+
+Dolores eirmod vulputate illum consectetuer sadipscing et wisi suscipit kasd. Voluptua eros sit sea lorem dolor elitr facilisi sed et luptatum congue magna. Accusam voluptua dolores eirmod ut aliquyam wisi amet clita feugiat lorem ipsum sit dolor at dolore erat aliquyam aliquyam. Euismod dolor ut sed diam amet eos. Labore hendrerit dolor facilisi sadipscing illum. Ut voluptua nibh lorem no et at. Consectetuer lorem ex laoreet justo diam euismod zzril dolore enim ex accusam ea. Voluptua ut eros sea et amet et dolore rebum at iriure vero nonumy clita sed rebum. Lorem justo no magna et invidunt nonumy erat et. Eos autem nonumy accumsan nonumy ipsum duo et sanctus eos voluptua sit id nostrud ipsum no accusam. Ea nulla amet accumsan takimata et duis dolore sed consectetuer accumsan lorem ullamcorper exerci sanctus at lorem magna euismod. Sit no et et invidunt sed rebum ipsum consetetur blandit duo clita rebum at ipsum nisl vero et iriure. Vero sea takimata dolore ipsum. Vel autem amet justo quis est et dolores duis est nisl et hendrerit lorem amet consequat sanctus et dolor. Labore erat est ut ut zzril sea enim aliquyam quod. Eirmod kasd sed vulputate luptatum sea at sea rebum sed ipsum duis ipsum sit tincidunt erat mazim est. Wisi ut ea voluptua vero diam tincidunt gubergren et kasd sit.
+
+Lorem ipsum qui takimata kasd. Accusam nonumy invidunt zzril lorem invidunt invidunt ut diam labore kasd ut ea ad. Erat duo nam ut est diam eirmod dolor sed erat. Diam amet sadipscing clita lorem praesent vulputate amet labore takimata erat lorem amet et stet. Dolor voluptua eos voluptua magna nisl ipsum voluptua ut rebum lorem. Diam et sadipscing autem ipsum tempor gubergren te veniam quis qui sanctus et sit. Duis amet sed kasd minim praesent et invidunt lorem justo est tempor. Sed et diam invidunt diam magna takimata sit dolor commodo amet nulla dolore. Elitr eos sea consetetur feugiat zzril diam. Voluptua et dolor erat velit takimata. Eirmod ut ipsum adipiscing et qui sed lorem sit ut dolore sed at autem gubergren vel dignissim. Takimata sed stet dolor vero voluptua possim sed ut sea vulputate sit sed nostrud duo dolor no. Dolor eos clita. Sed aliquip in feugiat elitr aliquyam praesent elitr sadipscing et amet lorem eu amet et tation praesent et. Hendrerit labore stet clita dolores amet voluptua sea nonumy ipsum at consetetur duis amet magna sadipscing sadipscing. Wisi ut ut facilisis sadipscing rebum dolor et illum diam dolor. Et sadipscing magna at ut eros sit takimata vero in lorem ipsum.
+
+Dignissim dolor lorem magna praesent eos sed est tempor invidunt amet dolor. Eos iriure volutpat ipsum lorem diam nisl est lorem stet labore dolores est feugiat. Adipiscing velit suscipit ipsum dolor vulputate minim elitr nulla tempor gubergren delenit rebum. Et nonummy et laoreet ipsum nonumy magna at aliquam lorem hendrerit vero dolores laoreet aliquyam cum ea. At ipsum hendrerit illum zzril elitr erat consectetuer nibh eirmod. Amet vulputate at nonumy et diam. Sit minim consequat nibh nonumy sanctus sit sanctus amet at accumsan diam dolore kasd accusam. Nonumy ullamcorper suscipit feugait aliquyam sed sed blandit diam.
+
+Eirmod ullamcorper et ut accumsan autem nihil ipsum. Voluptua ipsum eleifend lorem aliquyam. Wisi aliquip clita dolor tation eros sed sadipscing volutpat.
+
+Velit stet no ut justo ipsum. Sit dolor ea sed tincidunt et ad rebum hendrerit et accusam sed no dolores erat est vel. Vulputate nonumy sit dolor no ut. Tempor dolor gubergren sea consetetur ea sea amet lorem invidunt labore erat eirmod eos aliquyam. Eum lorem no sanctus dolore diam lorem vero ea elit elitr sit no erat lorem tempor. Rebum eos stet lobortis. Volutpat ut rebum et aliquam magna sit. Takimata lorem amet autem dolor dolore amet kasd labore ea ipsum consequat amet doming consetetur magna hendrerit. Et nulla no vero adipiscing kasd ipsum at sit. Sadipscing sanctus praesent eos sed et gubergren nonumy sit diam. Eos velit esse rebum takimata et illum kasd et elitr consequat consectetuer ipsum et lorem stet duo diam.
+
+Erat magna diam dolor. Vel voluptua mazim laoreet ut est aliquyam dolore. Tincidunt sanctus euismod dolor in amet accusam consequat erat sit ut. Aliquip takimata clita no diam dolor accusam qui nonumy. Gubergren invidunt illum vel diam voluptua eirmod molestie clita est dolores delenit takimata eirmod takimata possim. Augue eirmod ea dolore at kasd eos sed et nonummy. No lorem nostrud vel eos velit justo velit consetetur gubergren. Iriure id in magna est accusam diam wisi at dignissim. Euismod sea feugiat stet ipsum labore et duis minim accusam enim. Augue ut eum aliquam elitr accusam justo blandit amet sanctus eirmod sadipscing et erat. Velit luptatum velit et et. Erat dolor sit. Sadipscing vel ut erat eleifend vel lorem lorem invidunt sed dolore ullamcorper ea minim clita sanctus duo gubergren ipsum. Eirmod sadipscing at justo invidunt kasd elitr et in amet sed ipsum stet nisl lorem at ut eirmod amet. Duo sed cum dolor aliquyam ea ipsum sea. Rebum at duis ipsum erat eum dolores accusam iriure sadipscing. Nulla ipsum velit invidunt invidunt ea aliquyam amet.
+
+Et aliquyam lorem iusto volutpat no nulla eos. Accusam stet ut stet nibh sed ipsum lorem dolore kasd et ipsum diam amet diam dolores ea sea. Luptatum rebum duo dolor feugait labore sadipscing amet. Amet iriure tempor nibh ipsum consectetuer et sed eos in gubergren nibh accusam kasd. Ut consequat magna dolore ut at aliquip ut dolor sed sed erat ipsum dolore consetetur sed sit dolor. Sea suscipit vero euismod lorem et vero et amet quod ipsum invidunt consetetur lorem est dolores voluptua dolore clita. Nonumy vulputate diam ut. In tation justo aliquyam vel veniam no amet accusam. Vero clita sea et tincidunt consetetur at te stet aliquam est dolor no dolore facilisis ut. Diam invidunt clita justo wisi magna diam dolor ea sanctus. Ea dolore aliquyam et vero ipsum stet est duo et justo adipiscing dolor ea sed dolore nibh stet ipsum.
+
+Ad sit justo dolor no labore stet ullamcorper duo. Diam eirmod takimata. Magna dolores ut diam eos invidunt magna dolores option accusam magna molestie ipsum molestie sadipscing aliquyam et. Ut duo dolor invidunt stet at. Dolor sit duo amet dolor et et in et dolor rebum sed diam vero vel amet imperdiet et. Diam quod magna vel rebum et invidunt gubergren ipsum takimata elitr amet dolor diam at in labore diam.
+
+Veniam diam sadipscing et est aliquyam duo dolor at eirmod ut. Ipsum diam erat facer eirmod et sanctus kasd kasd kasd diam labore sed. Dolores labore ea blandit. Dolore dolor stet sit iusto option. Dolor lorem est elitr lorem et et amet elitr takimata dolor cum ipsum sed. At sadipscing dolore sit gubergren sanctus ipsum stet labore quis at consectetuer lorem feugiat diam et feugait diam.
+
+Takimata nam duo et. Amet diam gubergren gubergren. Elit molestie sed accumsan diam hendrerit justo. Velit elitr sit tempor liber imperdiet tincidunt suscipit ut eirmod invidunt ut et augue.
+
+Consetetur justo tincidunt ut aliquyam kasd sed sadipscing dolor eu labore. Elitr sadipscing sed et ipsum dolores justo elit ut et sit at. Et ea accumsan quod at ut et eos lorem elit duo rebum dolore accusam. Rebum no labore voluptua clita. Rebum at no et kasd est consetetur nostrud takimata vero duo labore est justo diam eirmod erat diam. Dolores diam sadipscing augue gubergren suscipit amet sit euismod qui sed invidunt vero sed dolor takimata justo ea. Sadipscing aliquyam iusto feugiat quis sanctus eleifend. Dolores dolor ipsum te sed accusam magna. Gubergren eirmod stet lorem nonumy dolores tincidunt sit sadipscing dolor stet sadipscing accusam sed stet. Iusto vulputate labore elit feugiat no luptatum et nonumy vulputate dolore gubergren sed. Sit wisi sadipscing commodo. Duo diam luptatum vel diam sit et ut rebum ut nam no. Eos velit et aliquam est et dolores sit est et lorem assum. Volutpat consetetur duo dolor sanctus sit magna eos consetetur dolore accusam et sea sit no iriure accusam.
+
+Amet facilisis accusam esse ipsum ea gubergren. Amet dolor consectetuer diam clita consetetur tincidunt et feugait labore et. Sanctus consequat ipsum sed placerat ut sadipscing rebum justo voluptua nulla ut autem dolore. Ut duis ipsum ipsum minim nulla aliquam nonumy eirmod et voluptua no dolore commodo voluptua zzril. Lorem sed dolores augue eos sed ipsum lorem vero sit diam iriure no commodo hendrerit. Ipsum dolor iriure est. Ipsum eu dolore dolor ea et. Et consetetur elit quod tation duo magna. Nulla dolor tempor et hendrerit. Elitr illum imperdiet ea erat. Ut erat tincidunt consequat dolor dolore. Vel dolor nobis ad no sed sit dolor stet erat nobis. Diam praesent sanctus amet sit duis. Aliquyam sea consetetur dolore et nobis ea amet nonummy sit sit tempor sed ea duis no velit rebum diam. Sadipscing et dolore erat. Consetetur amet nonumy labore sed erat eu delenit et lorem consectetuer dolor rebum aliquyam. Sed iriure aliquip sit sanctus kasd kasd et erat diam dolores invidunt duo accusam autem est sadipscing ea.
+
+Duo erat dolores soluta dolores lorem tation amet illum rebum et at dolores clita tempor vel. Eu amet consetetur ea ad dolore dolor kasd. Wisi elitr ad amet. Tempor tempor justo ipsum nibh eu dolor ea et odio ea sit ea wisi voluptua justo. Labore et dolores te duis qui id et wisi lorem in vero eos veniam ipsum. At ad aliquam dolore rebum sadipscing diam cum aliquyam diam vulputate tempor lorem.
+
+Cum et sed sea ea diam minim magna ipsum hendrerit duis clita voluptua. Sed amet takimata duo diam diam dolor sit tempor at sed nihil. Nulla erat sed takimata et. Ipsum eirmod luptatum. Accusam vel imperdiet illum sed tempor diam. Magna sed at dolor takimata elit et laoreet in lorem et lorem labore et tempor stet autem erat. Clita in vero takimata elitr autem est invidunt qui amet. Diam eleifend augue et gubergren et velit diam consetetur.
+
+Dolores ea et accusam nihil clita sanctus aliquyam eu sed ut congue ipsum amet. Est luptatum et ipsum autem duo et ea clita duo nulla illum takimata et vero accusam tempor sed. Sadipscing tincidunt labore. Dolor sadipscing magna exerci praesent consetetur dolor ipsum vero sed sit minim eos volutpat diam consequat. Amet sit kasd et nonumy ea lorem stet no diam ipsum est et ipsum lorem et enim sed no. At hendrerit et no eos elit lorem tempor invidunt. Ut dolor exerci diam ipsum gubergren placerat amet sea minim magna amet et duo iusto eu sit. Labore gubergren nonummy tempor option et erat sanctus dolore justo facilisi invidunt sit elit sed takimata hendrerit duo vero. Lorem nonumy ipsum ea justo dolor vulputate. Stet dolor liber eleifend amet erat ea. Hendrerit elitr zzril facilisis autem et erat eos in ea dolores qui amet duo. Lorem nonumy eirmod accusam labore dolore stet takimata blandit augue vel vero sit feugiat takimata. Consectetuer ea duo consequat lorem odio dolor. Eum no at. Dolores ipsum dolores aliquyam amet at sit clita amet ut ea erat.
+
+Clita et dolores. Et in molestie voluptua feugiat est magna ut. Et sed diam eum invidunt suscipit takimata sed diam duis aliquip vel lorem dolore. Feugiat clita option diam et stet accumsan dolore sed stet. Labore wisi diam. Augue lorem in consequat dolor stet esse rebum. Elitr sanctus amet quod diam et labore vel nonumy sea. Diam et hendrerit rebum sit no no nulla sed dolor stet ut amet ut ipsum. Ipsum accusam erat et no vel nonumy minim et justo et nostrud rebum augue. Nonumy quis dolor rebum at sed nulla tempor consetetur sed aliquyam ipsum dolor et. Iriure tempor aliquyam ex sed hendrerit dolores ea est dolore dolore kasd dolor sed sadipscing enim consetetur dolore. Dolore vel tincidunt. Vero gubergren at labore justo rebum erat sed vero id duo vero sanctus takimata et. Nam iusto dolor diam magna nostrud erat eu tincidunt amet eu invidunt accusam dolor accumsan dolores. Nulla ea possim tempor takimata eos amet et et et dolor aliquyam accusam duis takimata.
+
+Kasd ullamcorper tempor duo vel labore vel sadipscing sed. Eleifend vero takimata commodo ut commodo duo minim elitr sadipscing. Autem no sea eu diam molestie eum amet nam. At vulputate et ut amet ea est amet. Sed kasd dolores iriure invidunt ipsum eos elitr zzril dolor ipsum et lorem consequat dolores stet ut sed consectetuer. Eum dolores euismod soluta sit voluptua ipsum ut nostrud consequat vulputate clita volutpat kasd sit. Dolor veniam eos illum lorem aliquyam dolore invidunt lobortis blandit diam eirmod sanctus eos ipsum. Eirmod consequat ea. Invidunt et sadipscing gubergren amet nonumy facilisis sed gubergren sed erat tempor consectetuer eum ea tempor nonumy dolor nonumy. Ut invidunt lorem labore lorem zzril clita velit tempor ex. Lorem dolore voluptua duo eu. Stet hendrerit duis ex ipsum stet minim.
+
+Accusam kasd sea rebum luptatum ipsum. Dolore nonumy qui amet gubergren kasd stet diam vero sed feugiat. Magna at vero sit sit labore et nulla in ut dolore et tempor duis voluptua rebum dolor. In et ipsum dolor consetetur aliquam lorem vel voluptua amet. Accusam diam clita ut erat vero ipsum eum sed vulputate eu sed facer diam sed in augue delenit ipsum. Sit quod feugait. Doming clita erat magna tempor consequat duo takimata eirmod tempor blandit ut. Tempor voluptua aliquam diam commodo ex sea. Elitr lorem at sit dolore tempor duo sed praesent at dolor dolores delenit sea dolores amet duo. Invidunt accusam odio ipsum invidunt nulla takimata vel et nonumy consetetur. Volutpat nonumy dolores sit ea. Et ut est kasd lorem erat et euismod voluptua vero sed vel elitr at gubergren.
+
+Tempor duis diam stet dolor diam illum amet commodo ut sea diam. Sit dolor ipsum aliquam hendrerit dolore liber no accusam in aliquyam et. Dolore at stet dolor ea duis voluptua in aliquyam magna sed dolores laoreet nonummy iriure ea. Autem dolor dolore lorem eos diam takimata tempor diam diam liber. Et et no et eros sea dolor eos dolor sit. Sit et accusam suscipit diam liber stet magna stet sadipscing et sit nobis diam takimata sadipscing dolores est. Tation et accusam magna rebum blandit justo magna ea aliquam. Commodo duo sea eirmod dolore dolore dolor lorem magna. Gubergren at iriure. Veniam at voluptua erat eirmod duis ut sadipscing. Diam tempor erat invidunt sed est no. Ipsum ipsum in sed duo et ipsum magna at in autem at dolore duo. Dolor labore ut sadipscing veniam sit imperdiet nonumy liber nostrud gubergren. Dolores eos sit elit dolore et suscipit. Nonummy ipsum diam duis accusam erat sed clita tempor est duo. Eros lorem ea invidunt eos stet wisi sea clita sea labore sit in vulputate erat eleifend at. Amet lorem ipsum et.
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/DirectCollisionTests.cs b/ModularPipelines.UnitTests/DirectCollisionTests.cs
index fb596b61a1..30c81bef65 100644
--- a/ModularPipelines.UnitTests/DirectCollisionTests.cs
+++ b/ModularPipelines.UnitTests/DirectCollisionTests.cs
@@ -27,11 +27,7 @@ public void Modules_Dependent_On_Each_Other_Throws_Exception()
[DependsOn]
private class DependencyConflictModule1 : Module
{
- public DependencyConflictModule1(IModuleContext moduleContext) : base(moduleContext)
- {
- }
-
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
await Task.CompletedTask;
return null;
@@ -41,11 +37,7 @@ public DependencyConflictModule1(IModuleContext moduleContext) : base(moduleCont
[DependsOn]
private class DependencyConflictModule2 : Module
{
- public DependencyConflictModule2(IModuleContext moduleContext) : base(moduleContext)
- {
- }
-
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
await Task.CompletedTask;
return null;
diff --git a/ModularPipelines.UnitTests/DisposableModuleTests.cs b/ModularPipelines.UnitTests/DisposableModuleTests.cs
index bbad47e62e..b4b8b31927 100644
--- a/ModularPipelines.UnitTests/DisposableModuleTests.cs
+++ b/ModularPipelines.UnitTests/DisposableModuleTests.cs
@@ -24,11 +24,8 @@ public async Task SuccessfullyDisposed()
public class DisposableModule : Module, IDisposable
{
public bool IsDisposed { get; private set; }
- public DisposableModule(IModuleContext context) : base(context)
- {
- }
- protected override async Task>?> ExecuteAsync(CancellationToken cancellationToken)
+ protected override async Task>?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
await Task.Delay(100, cancellationToken);
return null;
diff --git a/ModularPipelines.UnitTests/GlobalTestSettings.cs b/ModularPipelines.UnitTests/GlobalTestSettings.cs
new file mode 100644
index 0000000000..4952ec1211
--- /dev/null
+++ b/ModularPipelines.UnitTests/GlobalTestSettings.cs
@@ -0,0 +1 @@
+[assembly: Parallelizable(ParallelScope.All)]
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/Helpers/Base64Tests.cs b/ModularPipelines.UnitTests/Helpers/Base64Tests.cs
new file mode 100644
index 0000000000..036fbbd0fd
--- /dev/null
+++ b/ModularPipelines.UnitTests/Helpers/Base64Tests.cs
@@ -0,0 +1,76 @@
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.UnitTests.Helpers;
+
+public class Base64Tests : TestBase
+{
+ private class ToBase64Module : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return context.Base64.ToBase64String("Foo bar!");
+ }
+ }
+
+ [Test]
+ public async Task To_Base64_Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task To_Base64_Output_Equals_Foo_Bar()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.That(moduleResult.Value, Is.EqualTo("Rm9vIGJhciE="));
+ }
+
+ private class FromBase64Module : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return context.Base64.FromBase64String("Rm9vIGJhciE=");
+ }
+ }
+
+ [Test]
+ public async Task From_Base64_Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task From_Base64_Output_Equals_Foo_Bar()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.That(moduleResult.Value, Is.EqualTo("Foo bar!"));
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/Helpers/CmdTests.cs b/ModularPipelines.UnitTests/Helpers/CmdTests.cs
new file mode 100644
index 0000000000..9c6cbd4ca3
--- /dev/null
+++ b/ModularPipelines.UnitTests/Helpers/CmdTests.cs
@@ -0,0 +1,56 @@
+using CliWrap.Buffered;
+using ModularPipelines.Cmd.Extensions;
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.UnitTests.Helpers;
+
+public class CmdTests : TestBase
+{
+ [SetUp]
+ public void Setup()
+ {
+ if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+ {
+ Assert.Ignore("Cmd is Windows only");
+ }
+ }
+
+ private class CmdEchoModule : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ return await context.Cmd().Script(new("echo Foo bar!"), cancellationToken: cancellationToken);
+ }
+ }
+
+ [Test]
+ public async Task Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task Standard_Output_Equals_Foo_Bar()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.Value!.StandardError, Is.Null.Or.Empty);
+ Assert.That(moduleResult.Value.StandardOutput.Trim(), Is.EqualTo("Foo bar!"));
+ });
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/Helpers/CommandTests.cs b/ModularPipelines.UnitTests/Helpers/CommandTests.cs
new file mode 100644
index 0000000000..c9e471178c
--- /dev/null
+++ b/ModularPipelines.UnitTests/Helpers/CommandTests.cs
@@ -0,0 +1,50 @@
+using CliWrap.Buffered;
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+using ModularPipelines.Options;
+
+namespace ModularPipelines.UnitTests.Helpers;
+
+public class CommandTests : TestBase
+{
+ private class CommandEchoModule : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ return await context.Command.UsingCommandLineTool(new CommandLineToolOptions("pwsh")
+ {
+ Arguments = new []{ "-Command", "echo 'Foo bar!'" }
+ }, cancellationToken: cancellationToken);
+ }
+ }
+
+ [Test]
+ public async Task Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task Standard_Output_Equals_Foo_Bar()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.Value!.StandardError, Is.Null.Or.Empty);
+ Assert.That(moduleResult.Value.StandardOutput.Trim(), Is.EqualTo("Foo bar!"));
+ });
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/Helpers/DotNetTests.cs b/ModularPipelines.UnitTests/Helpers/DotNetTests.cs
new file mode 100644
index 0000000000..f9bbabd1fe
--- /dev/null
+++ b/ModularPipelines.UnitTests/Helpers/DotNetTests.cs
@@ -0,0 +1,47 @@
+using CliWrap.Buffered;
+using ModularPipelines.Context;
+using ModularPipelines.DotNet.Extensions;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.UnitTests.Helpers;
+
+public class DotNetTests : TestBase
+{
+ private class DotNetVersionModule : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ return await context.DotNet().Version(cancellationToken: cancellationToken);
+ }
+ }
+
+ [Test]
+ public async Task Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task Standard_Output_Starts_With_Git_Version()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.Value!.StandardError, Is.Null.Or.Empty);
+ Assert.That(moduleResult.Value.StandardOutput, Does.Match("\\d+"));
+ });
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/Helpers/GitTests.cs b/ModularPipelines.UnitTests/Helpers/GitTests.cs
new file mode 100644
index 0000000000..dbdf393718
--- /dev/null
+++ b/ModularPipelines.UnitTests/Helpers/GitTests.cs
@@ -0,0 +1,47 @@
+using CliWrap.Buffered;
+using ModularPipelines.Context;
+using ModularPipelines.Git.Extensions;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.UnitTests.Helpers;
+
+public class GitTests : TestBase
+{
+ private class GitVersionModule : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ return await context.Git().Operations.Version(cancellationToken: cancellationToken);
+ }
+ }
+
+ [Test]
+ public async Task Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task Standard_Output_Starts_With_Git_Version()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.Value!.StandardError, Is.Null.Or.Empty);
+ Assert.That(moduleResult.Value.StandardOutput, Does.Match("git version \\d+.*"));
+ });
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/Helpers/HexTests.cs b/ModularPipelines.UnitTests/Helpers/HexTests.cs
new file mode 100644
index 0000000000..998afe999f
--- /dev/null
+++ b/ModularPipelines.UnitTests/Helpers/HexTests.cs
@@ -0,0 +1,76 @@
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.UnitTests.Helpers;
+
+public class HexTests : TestBase
+{
+ private class ToHexModule : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return context.Hex.ToHex("Foo bar!");
+ }
+ }
+
+ [Test]
+ public async Task To_Hex_Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task To_Hex_Output_Equals_Foo_Bar()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.That(moduleResult.Value, Is.EqualTo("466f6f2062617221"));
+ }
+
+ private class FromHexModule : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return context.Hex.FromHex("466f6f2062617221");
+ }
+ }
+
+ [Test]
+ public async Task From_Hex_Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task From_Hex_Output_Equals_Foo_Bar()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.That(moduleResult.Value, Is.EqualTo("Foo bar!"));
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/Helpers/Md5Tests.cs b/ModularPipelines.UnitTests/Helpers/Md5Tests.cs
new file mode 100644
index 0000000000..45cd3dc38d
--- /dev/null
+++ b/ModularPipelines.UnitTests/Helpers/Md5Tests.cs
@@ -0,0 +1,42 @@
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.UnitTests.Helpers;
+
+public class Md5Tests : TestBase
+{
+ private class ToMd5Module : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return context.Hasher.Md5("Foo bar!");
+ }
+ }
+
+ [Test]
+ public async Task To_Md5_Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task To_Md5_Output_Equals_Foo_Bar()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.That(moduleResult.Value, Is.EqualTo("b9c291e3274aa5c8010a7c5c22a4e6dd"));
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/Helpers/NodeTests.cs b/ModularPipelines.UnitTests/Helpers/NodeTests.cs
new file mode 100644
index 0000000000..8dd4ccd621
--- /dev/null
+++ b/ModularPipelines.UnitTests/Helpers/NodeTests.cs
@@ -0,0 +1,54 @@
+using CliWrap.Buffered;
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+using ModularPipelines.Node.Extensions;
+
+namespace ModularPipelines.UnitTests.Helpers;
+
+public class NodeTests : TestBase
+{
+ private class NodeVersionModule : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ var task = () =>
+ {
+ context.Node().Npm.Install(null!, cancellationToken);
+ context.Node().Npm.Run(null!, cancellationToken);
+ context.Node().Nvm.Use(null!, cancellationToken);
+ };
+
+ return await context.Node().Version(cancellationToken: cancellationToken);
+ }
+ }
+
+ [Test]
+ public async Task Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task Standard_Output_Is_Version_Number()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.Value!.StandardError, Is.Null.Or.Empty);
+ Assert.That(moduleResult.Value.StandardOutput, Does.Match("v\\d+"));
+ });
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/Helpers/PowershellTests.cs b/ModularPipelines.UnitTests/Helpers/PowershellTests.cs
new file mode 100644
index 0000000000..9d46da5de8
--- /dev/null
+++ b/ModularPipelines.UnitTests/Helpers/PowershellTests.cs
@@ -0,0 +1,47 @@
+using CliWrap.Buffered;
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+using ModularPipelines.Powershell.Extensions;
+
+namespace ModularPipelines.UnitTests.Helpers;
+
+public class PowershellTests : TestBase
+{
+ private class PowershellEchoModule : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ return await context.Powershell().Script(new("Write-Host \"Foo bar!\""), cancellationToken: cancellationToken);
+ }
+ }
+
+ [Test]
+ public async Task Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task Standard_Output_Equals_Foo_Bar()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.Value!.StandardError, Is.Null.Or.Empty);
+ Assert.That(moduleResult.Value.StandardOutput.Trim(), Is.EqualTo("Foo bar!"));
+ });
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/Helpers/Sha1Tests.cs b/ModularPipelines.UnitTests/Helpers/Sha1Tests.cs
new file mode 100644
index 0000000000..5342814f74
--- /dev/null
+++ b/ModularPipelines.UnitTests/Helpers/Sha1Tests.cs
@@ -0,0 +1,42 @@
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.UnitTests.Helpers;
+
+public class Sha1Tests : TestBase
+{
+ private class ToSha1Module : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return context.Hasher.Sha1("Foo bar!");
+ }
+ }
+
+ [Test]
+ public async Task To_Sha1_Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task To_Sha1_Output_Equals_Foo_Bar()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.That(moduleResult.Value, Is.EqualTo("cc3626c5ad2e3aff0779dc63e80555c463fd99dc"));
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/Helpers/Sha256Tests.cs b/ModularPipelines.UnitTests/Helpers/Sha256Tests.cs
new file mode 100644
index 0000000000..fdceb33c24
--- /dev/null
+++ b/ModularPipelines.UnitTests/Helpers/Sha256Tests.cs
@@ -0,0 +1,42 @@
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.UnitTests.Helpers;
+
+public class Sha256Tests : TestBase
+{
+ private class ToSha256Module : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return context.Hasher.Sha256("Foo bar!");
+ }
+ }
+
+ [Test]
+ public async Task To_Sha256_Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task To_Sha256_Output_Equals_Foo_Bar()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.That(moduleResult.Value, Is.EqualTo("d80c14a132a9ae008c78db4ee4cbc46b015b5e0f018f6b0a3e4ea5041176b852"));
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/Helpers/Sha384Tests.cs b/ModularPipelines.UnitTests/Helpers/Sha384Tests.cs
new file mode 100644
index 0000000000..8576de149d
--- /dev/null
+++ b/ModularPipelines.UnitTests/Helpers/Sha384Tests.cs
@@ -0,0 +1,42 @@
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.UnitTests.Helpers;
+
+public class Sha384Tests : TestBase
+{
+ private class ToSha384Module : Module
+ {
+ protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+ return context.Hasher.Sha384("Foo bar!");
+ }
+ }
+
+ [Test]
+ public async Task To_Sha384_Has_Not_Errored()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(moduleResult.ModuleResultType, Is.EqualTo(ModuleResultType.SuccessfulResult));
+ Assert.That(moduleResult.Exception, Is.Null);
+ Assert.That(moduleResult.Value, Is.Not.Null);
+ });
+ }
+
+ [Test]
+ public async Task To_Sha384_Output_Equals_Foo_Bar()
+ {
+ var module = await RunModule();
+
+ var moduleResult = await module;
+
+ Assert.That(moduleResult.Value, Is.EqualTo("bb338a277da65d5663467d5fd98aa67349506150cd1287597b0eaa0f0988d2b22c33504fd85dd0b8c99ce8cc50666f88"));
+ }
+}
\ No newline at end of file
diff --git a/ModularPipelines.UnitTests/Helpers/Sha512Tests.cs b/ModularPipelines.UnitTests/Helpers/Sha512Tests.cs
new file mode 100644
index 0000000000..1685cae398
--- /dev/null
+++ b/ModularPipelines.UnitTests/Helpers/Sha512Tests.cs
@@ -0,0 +1,42 @@
+using ModularPipelines.Context;
+using ModularPipelines.Models;
+using ModularPipelines.Modules;
+
+namespace ModularPipelines.UnitTests.Helpers;
+
+public class Sha512Tests : TestBase
+{
+ private class ToSha512Module : Module