diff --git a/.circleci/scripts/config-template.yml b/.circleci/scripts/config-template.yml
index 1acc8a2280..d02df9246f 100644
--- a/.circleci/scripts/config-template.yml
+++ b/.circleci/scripts/config-template.yml
@@ -116,7 +116,6 @@ jobs: # Each project will have individual jobs for each specific task it has to
test-core:
machine:
image: ubuntu-2204:2023.02.1
- docker_layer_caching: true
resource_class: large
steps:
- cached-checkout
@@ -131,6 +130,9 @@ jobs: # Each project will have individual jobs for each specific task it has to
- run-tests:
title: Core Integration Tests
project: Core/IntegrationTests/TestsIntegration.csproj
+ - run-tests:
+ title: Automate Integration Tests
+ project: Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/Speckle.Automate.Sdk.Tests.Integration.csproj
- store_test_results:
path: TestResults
diff --git a/All.sln b/All.sln
index 0ac1813031..2ad59e31d7 100644
--- a/All.sln
+++ b/All.sln
@@ -167,6 +167,7 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConnectorCSIBridge", "ConnectorCSI\ConnectorCSIBridge\ConnectorCSIBridge.csproj", "{23BE6E54-96C1-4373-89F3-E18A1C9807FD}"
ProjectSection(ProjectDependencies) = postProject
{60BE029E-1F31-4473-8B68-A745A43AF179} = {60BE029E-1F31-4473-8B68-A745A43AF179}
+ {21223BA5-C6E8-405D-B581-106C4726EDC0} = {21223BA5-C6E8-405D-B581-106C4726EDC0}
EndProjectSection
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "ConnectorCSIShared", "ConnectorCSI\ConnectorCSIShared\ConnectorCSIShared.shproj", "{61374CD0-E774-4DCD-BFAB-6356B0931283}"
@@ -174,16 +175,19 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConnectorETABS", "ConnectorCSI\ConnectorETABS\ConnectorETABS.csproj", "{81299D15-5788-414D-A962-1A568C251323}"
ProjectSection(ProjectDependencies) = postProject
{60BE029E-1F31-4473-8B68-A745A43AF179} = {60BE029E-1F31-4473-8B68-A745A43AF179}
+ {D06F557C-452A-4BBE-9B79-A10DB03F3832} = {D06F557C-452A-4BBE-9B79-A10DB03F3832}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConnectorSAFE", "ConnectorCSI\ConnectorSAFE\ConnectorSAFE.csproj", "{9D188843-8841-4A76-A844-EFBE8E32EE05}"
ProjectSection(ProjectDependencies) = postProject
{60BE029E-1F31-4473-8B68-A745A43AF179} = {60BE029E-1F31-4473-8B68-A745A43AF179}
+ {442116F3-0F4A-4136-894E-FF5F4295500B} = {442116F3-0F4A-4136-894E-FF5F4295500B}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConnectorSAP2000", "ConnectorCSI\ConnectorSAP2000\ConnectorSAP2000.csproj", "{31E0C098-6813-4571-AB96-A245E0FC1C23}"
ProjectSection(ProjectDependencies) = postProject
{60BE029E-1F31-4473-8B68-A745A43AF179} = {60BE029E-1F31-4473-8B68-A745A43AF179}
+ {907AED7A-719B-4157-8CC9-D21CB26E9243} = {907AED7A-719B-4157-8CC9-D21CB26E9243}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DriverCSharp", "ConnectorCSI\DriverCSharp\DriverCSharp.csproj", "{C091E499-597D-4077-B83F-08E069091090}"
@@ -353,8 +357,14 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RevitSharedResources2024", "ConnectorRevit\RevitSharedResources2024\RevitSharedResources2024.csproj", "{C2BA8B6B-72BD-4DAB-865F-90C66083BDB2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConnectorAutocad2024", "ConnectorAutocadCivil\ConnectorAutocad2024\ConnectorAutocad2024.csproj", "{658DE496-5177-4CD5-A949-FE59E47109B6}"
+ ProjectSection(ProjectDependencies) = postProject
+ {1F21E740-6B05-47BD-8D2A-C9ED5E91C577} = {1F21E740-6B05-47BD-8D2A-C9ED5E91C577}
+ EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConnectorCivil2024", "ConnectorAutocadCivil\ConnectorCivil2024\ConnectorCivil2024.csproj", "{3E30D170-3CB4-4728-97D5-887C5019DA9B}"
+ ProjectSection(ProjectDependencies) = postProject
+ {B4D6F6DC-0712-4F9F-A24F-6B76DAE84B6F} = {B4D6F6DC-0712-4F9F-A24F-6B76DAE84B6F}
+ EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConverterAutocad2024", "Objects\Converters\ConverterAutocadCivil\ConverterAutocad2024\ConverterAutocad2024.csproj", "{1F21E740-6B05-47BD-8D2A-C9ED5E91C577}"
EndProject
@@ -372,6 +382,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConverterAdvanceSteel2024",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConverterDynamoRevit2024", "Objects\Converters\ConverterDynamo\ConverterDynamoRevit2024\ConverterDynamoRevit2024.csproj", "{75144587-6F51-46C8-8E40-DA652FBC53F4}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Automate.Sdk", "Automate\Speckle.Automate.Sdk\Speckle.Automate.Sdk.csproj", "{AF51DD10-C0D5-4209-AF55-8F6476EA8A99}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Automate", "Automate", "{F7399C6A-0EA4-4212-A49E-0342BED82F98}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C6FF0E4F-38A3-4464-98E9-AB71D74B06F4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Automate.Sdk.Tests.Integration", "Automate\Tests\Speckle.Automate.Sdk.Tests.Integration\Speckle.Automate.Sdk.Tests.Integration.csproj", "{A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ConnectorCore", "ConnectorCore", "{DA9DFC36-C53F-4B19-8911-BF7605230BA7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BatchUploader.OperationDriver", "ConnectorCore\BatchUploader.OperationDriver\BatchUploader.OperationDriver.csproj", "{7F0206A9-61D4-4D3A-9B43-789DABA7C143}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BatchUploader.Sdk", "ConnectorCore\BatchUploader.Sdk\BatchUploader.Sdk.csproj", "{2CC777EB-BD63-4FAB-BC3A-68A640D2E639}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug Mac|Any CPU = Debug Mac|Any CPU
@@ -2057,6 +2081,70 @@ Global
{75144587-6F51-46C8-8E40-DA652FBC53F4}.Release|Any CPU.Build.0 = Release|Any CPU
{75144587-6F51-46C8-8E40-DA652FBC53F4}.Release|x64.ActiveCfg = Release|Any CPU
{75144587-6F51-46C8-8E40-DA652FBC53F4}.Release|x64.Build.0 = Release|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Debug Mac|Any CPU.ActiveCfg = Debug|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Debug Mac|Any CPU.Build.0 = Debug|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Debug Mac|x64.ActiveCfg = Debug|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Debug Mac|x64.Build.0 = Debug|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Debug|x64.Build.0 = Debug|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Release Mac|Any CPU.ActiveCfg = Debug|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Release Mac|Any CPU.Build.0 = Debug|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Release Mac|x64.ActiveCfg = Debug|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Release Mac|x64.Build.0 = Debug|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Release|x64.ActiveCfg = Release|Any CPU
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99}.Release|x64.Build.0 = Release|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Debug Mac|Any CPU.ActiveCfg = Debug|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Debug Mac|Any CPU.Build.0 = Debug|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Debug Mac|x64.ActiveCfg = Debug|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Debug Mac|x64.Build.0 = Debug|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Debug|x64.Build.0 = Debug|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Release Mac|Any CPU.ActiveCfg = Debug|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Release Mac|Any CPU.Build.0 = Debug|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Release Mac|x64.ActiveCfg = Debug|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Release Mac|x64.Build.0 = Debug|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Release|x64.ActiveCfg = Release|Any CPU
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4}.Release|x64.Build.0 = Release|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Debug Mac|Any CPU.ActiveCfg = Debug|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Debug Mac|Any CPU.Build.0 = Debug|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Debug Mac|x64.ActiveCfg = Debug|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Debug Mac|x64.Build.0 = Debug|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Debug|x64.Build.0 = Debug|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Release Mac|Any CPU.ActiveCfg = Debug|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Release Mac|Any CPU.Build.0 = Debug|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Release Mac|x64.ActiveCfg = Debug|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Release Mac|x64.Build.0 = Debug|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Release|x64.ActiveCfg = Release|Any CPU
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143}.Release|x64.Build.0 = Release|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Debug Mac|Any CPU.ActiveCfg = Debug|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Debug Mac|Any CPU.Build.0 = Debug|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Debug Mac|x64.ActiveCfg = Debug|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Debug Mac|x64.Build.0 = Debug|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Debug|x64.Build.0 = Debug|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Release Mac|Any CPU.ActiveCfg = Debug|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Release Mac|Any CPU.Build.0 = Debug|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Release Mac|x64.ActiveCfg = Debug|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Release Mac|x64.Build.0 = Debug|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Release|x64.ActiveCfg = Release|Any CPU
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2214,6 +2302,11 @@ Global
{3B9189B9-E485-448A-8793-9B9587A36791} = {7B7C4CB1-3D60-4A5B-9902-C812521A24B3}
{737D5567-7B1F-410D-9B7B-BAE8065ED15B} = {BE521908-7944-46F3-98BF-B47D34509934}
{75144587-6F51-46C8-8E40-DA652FBC53F4} = {F0DD5C38-083B-43EA-8654-96247028D8AC}
+ {AF51DD10-C0D5-4209-AF55-8F6476EA8A99} = {F7399C6A-0EA4-4212-A49E-0342BED82F98}
+ {C6FF0E4F-38A3-4464-98E9-AB71D74B06F4} = {F7399C6A-0EA4-4212-A49E-0342BED82F98}
+ {A0C9EBE0-A56A-4D07-B6EF-2EEAEC45D6C4} = {C6FF0E4F-38A3-4464-98E9-AB71D74B06F4}
+ {7F0206A9-61D4-4D3A-9B43-789DABA7C143} = {DA9DFC36-C53F-4B19-8911-BF7605230BA7}
+ {2CC777EB-BD63-4FAB-BC3A-68A640D2E639} = {DA9DFC36-C53F-4B19-8911-BF7605230BA7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1D43D91B-4F01-4A78-8250-CC6F9BD93A14}
diff --git a/All.sln.DotSettings b/All.sln.DotSettings
index 531edb4e6d..bd21bc95e3 100644
--- a/All.sln.DotSettings
+++ b/All.sln.DotSettings
@@ -67,9 +67,11 @@
Speckle:Cleanup
+ CSI
GQL
QL
SQ
UI
ExternalToolData|CSharpier|csharpier||csharpier|$FILE$
- CamelCase
\ No newline at end of file
+ CamelCase
+ True
\ No newline at end of file
diff --git a/Automate/Speckle.Automate.Sdk/AutomationContext.cs b/Automate/Speckle.Automate.Sdk/AutomationContext.cs
new file mode 100644
index 0000000000..d3038244ab
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/AutomationContext.cs
@@ -0,0 +1,310 @@
+# nullable enable
+using System.Diagnostics;
+using GraphQL;
+using Serilog.Debugging;
+using Speckle.Automate.Sdk.Schema;
+using Speckle.Core.Api;
+using Speckle.Core.Credentials;
+using Speckle.Core.Models;
+using Speckle.Core.Transports;
+using Speckle.Newtonsoft.Json;
+using Speckle.Newtonsoft.Json.Serialization;
+
+namespace Speckle.Automate.Sdk;
+
+public class AutomationContext
+{
+ public AutomationRunData AutomationRunData { get; set; }
+ public string? ContextView => AutomationResult.ResultView;
+ public Client SpeckleClient { get; set; }
+
+ private ServerTransport serverTransport;
+ private string speckleToken;
+
+ // keep a memory transport at hand, to speed up things if needed
+ private MemoryTransport memoryTransport;
+
+ // added for performance measuring
+ private Stopwatch initTime;
+
+ internal AutomationResult AutomationResult { get; set; }
+
+ public static async Task Initialize(AutomationRunData automationRunData, string speckleToken)
+ {
+ var account = new Account
+ {
+ token = speckleToken,
+ serverInfo = new ServerInfo { url = automationRunData.SpeckleServerUrl }
+ };
+ await account.Validate().ConfigureAwait(false);
+ var client = new Client(account);
+ var serverTransport = new ServerTransport(account, automationRunData.ProjectId);
+ var initTime = new Stopwatch();
+ initTime.Start();
+
+ return new AutomationContext
+ {
+ AutomationRunData = automationRunData,
+ SpeckleClient = client,
+ serverTransport = serverTransport,
+ speckleToken = speckleToken,
+ memoryTransport = new MemoryTransport(),
+ initTime = initTime,
+ AutomationResult = new AutomationResult(),
+ };
+ }
+
+ public static async Task Initialize(string automationRunData, string speckleToken)
+ {
+ var runData = JsonConvert.DeserializeObject(
+ automationRunData,
+ new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }
+ );
+ return await Initialize(runData, speckleToken).ConfigureAwait(false);
+ }
+
+ public string RunStatus => AutomationResult.RunStatus;
+
+ public string? StatusMessage => AutomationResult.StatusMessage;
+ public TimeSpan Elapsed => initTime.Elapsed;
+
+ public async Task ReceiveVersion()
+ {
+ var commit = await SpeckleClient
+ .CommitGet(AutomationRunData.ProjectId, AutomationRunData.VersionId)
+ .ConfigureAwait(false);
+ var commitRootObject = await Operations
+ .Receive(commit.referencedObject, serverTransport, memoryTransport)
+ .ConfigureAwait(false);
+ if (commitRootObject == null)
+ throw new Exception("Commit root object was null");
+ Console.WriteLine(
+ $"It took {Elapsed.TotalSeconds} seconds to receive the speckle version {AutomationRunData.VersionId}"
+ );
+ return commitRootObject;
+ }
+
+ public async Task CreateNewVersionInProject(Base rootObject, string branchName, string versionMessage = "")
+ {
+ if (branchName == AutomationRunData.BranchName)
+ throw new ArgumentException(
+ $"The target model: {branchName} cannot match the model that triggered this automation: {AutomationRunData.ModelId}/{AutomationRunData.BranchName}",
+ nameof(branchName)
+ );
+ var rootObjectId = await Operations
+ .Send(rootObject, new List { serverTransport, memoryTransport }, useDefaultCache: false)
+ .ConfigureAwait(false);
+
+ var branch = await SpeckleClient.BranchGet(AutomationRunData.ProjectId, branchName).ConfigureAwait(false);
+ if (branch is null)
+ {
+ // Create the branch with the specified name
+ await SpeckleClient
+ .BranchCreate(new BranchCreateInput() { streamId = AutomationRunData.ProjectId, name = branchName })
+ .ConfigureAwait(false);
+ }
+ var versionId = await SpeckleClient
+ .CommitCreate(
+ new CommitCreateInput
+ {
+ streamId = AutomationRunData.ProjectId,
+ branchName = branchName,
+ objectId = rootObjectId,
+ message = versionMessage,
+ }
+ )
+ .ConfigureAwait(false);
+ return versionId;
+ }
+
+ public void SetContextView(List? resourceIds = null, bool includeSourceModelVersion = true)
+ {
+ var linkResources = new List();
+ if (includeSourceModelVersion)
+ linkResources.Add($@"{AutomationRunData.ModelId}@{AutomationRunData.VersionId}");
+ if (resourceIds is not null)
+ linkResources.AddRange(resourceIds);
+ if (linkResources.Count == 0)
+ throw new Exception("We do not have enough resource ids to compose a context view");
+
+ AutomationResult.ResultView = $"/projects/{AutomationRunData.ProjectId}/models/{string.Join(",", linkResources)}";
+ }
+
+ public async Task ReportRunStatus()
+ {
+ ObjectResults? objectResults = null;
+ if (RunStatus is "SUCCEEDED" or "FAILED")
+ {
+ objectResults = new ObjectResults
+ {
+ Values = new ObjectResultValues
+ {
+ BlobIds = AutomationResult.Blobs,
+ ObjectResults = AutomationResult.ObjectResults
+ }
+ };
+ }
+ var request = new GraphQLRequest
+ {
+ Query =
+ @"
+ mutation ReportFunctionRunStatus(
+ $automationId: String!,
+ $automationRevisionId: String!,
+ $automationRunId: String!,
+ $versionId: String!,
+ $functionId: String!,
+ $functionName: String!,
+ $functionLogo: String,
+ $runStatus: AutomationRunStatus!
+ $elapsed: Float!
+ $resultVersionIds: [String!]!
+ $statusMessage: String
+ $objectResults: JSONObject
+ ){
+ automationMutations {
+ functionRunStatusReport(input: {
+ automationId: $automationId
+ automationRevisionId: $automationRevisionId
+ automationRunId: $automationRunId
+ versionId: $versionId
+ functionRuns: [{
+ functionId: $functionId,
+ functionName: $functionName,
+ functionLogo: $functionLogo,
+ status: $runStatus,
+ elapsed: $elapsed,
+ resultVersionIds: $resultVersionIds,
+ statusMessage: $statusMessage,
+ results: $objectResults,
+ }]
+ })
+ }
+ }
+ ",
+ Variables = new
+ {
+ automationId = AutomationRunData.AutomationId,
+ automationRevisionId = AutomationRunData.AutomationRevisionId,
+ automationRunId = AutomationRunData.AutomationRunId,
+ versionId = AutomationRunData.VersionId,
+ functionId = AutomationRunData.FunctionId,
+ functionName = AutomationRunData.FunctionName,
+ functionLogo = AutomationRunData.FunctionLogo,
+ runStatus = RunStatus,
+ statusMessage = AutomationResult.StatusMessage,
+ elapsed = Elapsed.TotalSeconds,
+ resultVersionIds = AutomationResult.ResultVersions,
+ objectResults,
+ }
+ };
+ await SpeckleClient.ExecuteGraphQLRequest>(request).ConfigureAwait(false);
+ }
+
+ public async Task StoreFileResult(string filePath)
+ {
+ if (!File.Exists(filePath))
+ throw new FileNotFoundException("The given file path doesn't exist", fileName: filePath);
+ using var formData = new MultipartFormDataContent();
+
+ var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
+ using var streamContent = new StreamContent(fileStream);
+ formData.Add(streamContent, "files", Path.GetFileName(filePath));
+ var request = await SpeckleClient.GQLClient.HttpClient
+ .PostAsync(
+ new Uri($"{AutomationRunData.SpeckleServerUrl}/api/stream/{AutomationRunData.ProjectId}/blob"),
+ formData
+ )
+ .ConfigureAwait(false);
+ request.EnsureSuccessStatusCode();
+ var responseString = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
+ Console.WriteLine("RESPONSE - " + responseString);
+ var uploadResponse = JsonConvert.DeserializeObject(responseString);
+ if (uploadResponse.UploadResults.Count != 1)
+ throw new Exception("Expected one upload result.");
+ AutomationResult.Blobs.AddRange(uploadResponse.UploadResults.Select(r => r.BlobId));
+ }
+
+ private void _markRun(AutomationStatus status, string? statusMessage)
+ {
+ var duration = Elapsed.TotalSeconds;
+ AutomationResult.StatusMessage = statusMessage;
+ var statusValue = AutomationStatusMapping.Get(status);
+ AutomationResult.RunStatus = statusValue;
+ AutomationResult.Elapsed = duration;
+
+ var msg = $"Automation run {statusValue} after {duration} seconds.";
+ if (statusMessage is not null)
+ msg += $"\n{statusMessage}";
+ Console.WriteLine(msg);
+ }
+
+ public void MarkRunFailed(string statusMessage)
+ {
+ _markRun(AutomationStatus.Failed, statusMessage);
+ }
+
+ public void MarkRunSuccess(string? statusMessage)
+ {
+ _markRun(AutomationStatus.Succeeded, statusMessage);
+ }
+
+ public void AttachErrorToObjects(
+ string category,
+ IEnumerable objectIds,
+ string? message = null,
+ Dictionary? metadata = null,
+ Dictionary? visualOverrides = null
+ )
+ {
+ AttachResultToObjects(ObjectResultLevel.Error, category, objectIds, message, metadata, visualOverrides);
+ }
+
+ public void AttachWarningToObjects(
+ string category,
+ IEnumerable objectIds,
+ string? message = null,
+ Dictionary? metadata = null,
+ Dictionary? visualOverrides = null
+ )
+ {
+ AttachResultToObjects(ObjectResultLevel.Warning, category, objectIds, message, metadata, visualOverrides);
+ }
+
+ public void AttachInfoToObjects(
+ string category,
+ IEnumerable objectIds,
+ string? message = null,
+ Dictionary? metadata = null,
+ Dictionary? visualOverrides = null
+ )
+ {
+ AttachResultToObjects(ObjectResultLevel.Info, category, objectIds, message, metadata, visualOverrides);
+ }
+
+ public void AttachResultToObjects(
+ ObjectResultLevel level,
+ string category,
+ IEnumerable objectIds,
+ string? message = null,
+ Dictionary? metadata = null,
+ Dictionary? visualOverrides = null
+ )
+ {
+ var levelString = ObjectResultLevelMapping.Get(level);
+ var objectIdList = objectIds.ToList();
+ Console.WriteLine($"Created new {levelString.ToUpper()} category: {category} caused by: {message}");
+
+ var resultCase = new ResultCase
+ {
+ Category = category,
+ Level = levelString,
+ ObjectIds = objectIdList,
+ Message = message,
+ Metadata = metadata,
+ VisualOverrides = visualOverrides
+ };
+
+ AutomationResult.ObjectResults.Add(resultCase);
+ }
+}
diff --git a/Automate/Speckle.Automate.Sdk/Runner.cs b/Automate/Speckle.Automate.Sdk/Runner.cs
new file mode 100644
index 0000000000..45adb74fa0
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/Runner.cs
@@ -0,0 +1,149 @@
+using System.CommandLine;
+using System.Diagnostics.CodeAnalysis;
+using Newtonsoft.Json.Schema.Generation;
+using Newtonsoft.Json.Serialization;
+using Speckle.Automate.Sdk.Schema;
+using Speckle.Newtonsoft.Json;
+
+namespace Speckle.Automate.Sdk;
+
+///
+/// Provides mechanisms to execute any function that conforms to the AutomateFunction "interface"
+///
+public static class AutomationRunner
+{
+ [SuppressMessage("Design", "CA1031:Do not catch general exception types")]
+ public static async Task RunFunction(
+ Func automateFunction,
+ AutomationRunData automationRunData,
+ string speckleToken,
+ TInput inputs
+ )
+ where TInput : struct
+ {
+ var automationContext = await AutomationContext.Initialize(automationRunData, speckleToken).ConfigureAwait(false);
+
+ try
+ {
+ await automateFunction(automationContext, inputs).ConfigureAwait(false);
+ if (automationContext.RunStatus is not ("FAILED" or "SUCCEEDED"))
+ automationContext.MarkRunSuccess(
+ "WARNING: Automate assumed a success status, but it was not marked as so by the function."
+ );
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex.ToString());
+ automationContext.MarkRunFailed("Function error. Check the automation run logs for details.");
+ }
+ finally
+ {
+ if (automationContext.ContextView is null)
+ automationContext.SetContextView();
+
+ await automationContext.ReportRunStatus().ConfigureAwait(false);
+ }
+ return automationContext;
+ }
+
+ public static async Task RunFunction(
+ Func automateFunction,
+ AutomationRunData automationRunData,
+ string speckleToken
+ )
+ {
+ return await RunFunction(
+ async (context, _) => await automateFunction(context).ConfigureAwait(false),
+ automationRunData,
+ speckleToken,
+ new Fake()
+ )
+ .ConfigureAwait(false);
+ }
+
+ private struct Fake { }
+
+ ///
+ /// Main entrypoint to execute an Automate function with no input data
+ ///
+ /// The command line arguments passed into the function by automate
+ /// The automate function to execute
+ /// This should always be called in your own functions, as it contains the logic to trigger the function automatically.
+ public static async Task Main(string[] args, Func automateFunction)
+ {
+ return await Main(
+ args,
+ async (AutomationContext context, Fake _) => await automateFunction(context).ConfigureAwait(false)
+ )
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Main entrypoint to execute an Automate function with input data of type
+ ///
+ /// The command line arguments passed into the function by automate
+ /// The automate function to execute
+ /// The provided input data
+ /// This should always be called in your own functions, as it contains the logic to trigger the function automatically.
+ public static async Task Main(string[] args, Func automateFunction)
+ where TInput : struct
+ {
+ var returnCode = 0; // This is the CLI return code, defaults to 0 (Success), change to 1 to flag a failed run.
+
+ var speckleProjectDataArg = new Argument(
+ name: "Speckle project data",
+ description: "The values of the project / model / version that triggered this function"
+ );
+ var functionInputsArg = new Argument(
+ name: "Function inputs",
+ description: "The values provided by the function user, matching the function input schema"
+ );
+ var speckleTokenArg = new Argument(
+ name: "Speckle token",
+ description: "A token to talk to the Speckle server with"
+ );
+ var rootCommand = new RootCommand();
+ rootCommand.AddArgument(speckleProjectDataArg);
+ rootCommand.AddArgument(functionInputsArg);
+ rootCommand.AddArgument(speckleTokenArg);
+ rootCommand.SetHandler(
+ async (speckleProjectData, functionInputs, speckleToken) =>
+ {
+ var automationRunData = JsonConvert.DeserializeObject(speckleProjectData);
+ var functionInputsParsed = JsonConvert.DeserializeObject(functionInputs);
+
+ var context = await RunFunction(automateFunction, automationRunData, speckleToken, functionInputsParsed)
+ .ConfigureAwait(false);
+
+ if (context.RunStatus != AutomationStatusMapping.Get(AutomationStatus.Succeeded))
+ returnCode = 1; // Flag run as failed.
+ },
+ speckleProjectDataArg,
+ functionInputsArg,
+ speckleTokenArg
+ );
+
+ var schemaFilePathArg = new Argument(
+ name: "Function inputs file path",
+ description: "A token to talk to the Speckle server with"
+ );
+
+ var generateSchemaCommand = new Command("generate-schema", "Generate JSON schema for the function inputs");
+ generateSchemaCommand.AddArgument(schemaFilePathArg);
+ generateSchemaCommand.SetHandler(
+ async (schemaFilePath) =>
+ {
+ var generator = new JSchemaGenerator { ContractResolver = new CamelCasePropertyNamesContractResolver() };
+ var schema = generator.Generate(typeof(TInput));
+ schema.ToString(global::Newtonsoft.Json.Schema.SchemaVersion.Draft2019_09);
+ File.WriteAllText(schemaFilePath, schema.ToString());
+ },
+ schemaFilePathArg
+ );
+ rootCommand.Add(generateSchemaCommand);
+
+ await rootCommand.InvokeAsync(args).ConfigureAwait(false);
+
+ return returnCode;
+ }
+}
diff --git a/Automate/Speckle.Automate.Sdk/Schema/AutomationResult.cs b/Automate/Speckle.Automate.Sdk/Schema/AutomationResult.cs
new file mode 100644
index 0000000000..9501b235d8
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/Schema/AutomationResult.cs
@@ -0,0 +1,12 @@
+namespace Speckle.Automate.Sdk.Schema;
+
+public class AutomationResult
+{
+ public double Elapsed { get; set; }
+ public string? ResultView { get; set; }
+ public List ResultVersions { get; set; } = new();
+ public List Blobs { get; set; } = new();
+ public string RunStatus { get; set; } = AutomationStatusMapping.Get(AutomationStatus.Running);
+ public string? StatusMessage { get; set; }
+ public List ObjectResults { get; set; } = new();
+}
diff --git a/Automate/Speckle.Automate.Sdk/Schema/AutomationRunData.cs b/Automate/Speckle.Automate.Sdk/Schema/AutomationRunData.cs
new file mode 100644
index 0000000000..cc74fff330
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/Schema/AutomationRunData.cs
@@ -0,0 +1,21 @@
+# nullable enable
+namespace Speckle.Automate.Sdk.Schema;
+
+///
+///Values of the project, model and automation that triggere this function run.
+///
+public struct AutomationRunData
+{
+ public string ProjectId { get; set; }
+ public string ModelId { get; set; }
+ public string BranchName { get; set; }
+ public string VersionId { get; set; }
+ public string SpeckleServerUrl { get; set; }
+ public string AutomationId { get; set; }
+ public string AutomationRevisionId { get; set; }
+ public string AutomationRunId { get; set; }
+ public string FunctionId { get; set; }
+ public string FunctionRelease { get; set; }
+ public string FunctionName { get; set; }
+ public string? FunctionLogo { get; set; }
+}
diff --git a/Automate/Speckle.Automate.Sdk/Schema/AutomationStatus.cs b/Automate/Speckle.Automate.Sdk/Schema/AutomationStatus.cs
new file mode 100644
index 0000000000..b23ba0feaf
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/Schema/AutomationStatus.cs
@@ -0,0 +1,12 @@
+namespace Speckle.Automate.Sdk.Schema;
+
+///
+/// Set the status of the automation.
+///
+public enum AutomationStatus
+{
+ Initializing,
+ Running,
+ Failed,
+ Succeeded
+}
diff --git a/Automate/Speckle.Automate.Sdk/Schema/AutomationStatusMapping.cs b/Automate/Speckle.Automate.Sdk/Schema/AutomationStatusMapping.cs
new file mode 100644
index 0000000000..e4474b114a
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/Schema/AutomationStatusMapping.cs
@@ -0,0 +1,21 @@
+namespace Speckle.Automate.Sdk.Schema;
+
+public abstract class AutomationStatusMapping
+{
+ private const string Initializing = "INITIALIZING";
+ private const string Running = "RUNNING";
+ private const string Failed = "FAILED";
+ private const string Succeeded = "SUCCEEDED";
+
+ public static string Get(AutomationStatus status)
+ {
+ return status switch
+ {
+ AutomationStatus.Running => Running,
+ AutomationStatus.Failed => Failed,
+ AutomationStatus.Succeeded => Succeeded,
+ AutomationStatus.Initializing => Initializing,
+ _ => throw new ArgumentOutOfRangeException($"Not valid value for enum {status}")
+ };
+ }
+}
diff --git a/Automate/Speckle.Automate.Sdk/Schema/BlobUploadResponse.cs b/Automate/Speckle.Automate.Sdk/Schema/BlobUploadResponse.cs
new file mode 100644
index 0000000000..f87ef17645
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/Schema/BlobUploadResponse.cs
@@ -0,0 +1,6 @@
+namespace Speckle.Automate.Sdk.Schema;
+
+public struct BlobUploadResponse
+{
+ public List UploadResults { get; set; }
+}
diff --git a/Automate/Speckle.Automate.Sdk/Schema/ObjectResultLevel.cs b/Automate/Speckle.Automate.Sdk/Schema/ObjectResultLevel.cs
new file mode 100644
index 0000000000..b868f67880
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/Schema/ObjectResultLevel.cs
@@ -0,0 +1,8 @@
+namespace Speckle.Automate.Sdk.Schema;
+
+public enum ObjectResultLevel
+{
+ Info,
+ Warning,
+ Error
+}
diff --git a/Automate/Speckle.Automate.Sdk/Schema/ObjectResultLevelMapping.cs b/Automate/Speckle.Automate.Sdk/Schema/ObjectResultLevelMapping.cs
new file mode 100644
index 0000000000..3668fe65ba
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/Schema/ObjectResultLevelMapping.cs
@@ -0,0 +1,19 @@
+namespace Speckle.Automate.Sdk.Schema;
+
+public abstract class ObjectResultLevelMapping
+{
+ private const string Info = "INFO";
+ private const string Warning = "WARNING";
+ private const string Error = "ERROR";
+
+ public static string Get(ObjectResultLevel level)
+ {
+ return level switch
+ {
+ ObjectResultLevel.Error => Error,
+ ObjectResultLevel.Warning => Warning,
+ ObjectResultLevel.Info => Info,
+ _ => throw new ArgumentOutOfRangeException($"Not valid value for enum {level}")
+ };
+ }
+}
diff --git a/Automate/Speckle.Automate.Sdk/Schema/ObjectResultValues.cs b/Automate/Speckle.Automate.Sdk/Schema/ObjectResultValues.cs
new file mode 100644
index 0000000000..1fdec07f64
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/Schema/ObjectResultValues.cs
@@ -0,0 +1,7 @@
+namespace Speckle.Automate.Sdk.Schema;
+
+struct ObjectResultValues
+{
+ public List ObjectResults { get; set; }
+ public List BlobIds { get; set; }
+}
diff --git a/Automate/Speckle.Automate.Sdk/Schema/ObjectResults.cs b/Automate/Speckle.Automate.Sdk/Schema/ObjectResults.cs
new file mode 100644
index 0000000000..947c072b0c
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/Schema/ObjectResults.cs
@@ -0,0 +1,7 @@
+namespace Speckle.Automate.Sdk.Schema;
+
+struct ObjectResults
+{
+ public string Version => "1.0.0";
+ public ObjectResultValues Values { get; set; }
+}
diff --git a/Automate/Speckle.Automate.Sdk/Schema/ResultCase.cs b/Automate/Speckle.Automate.Sdk/Schema/ResultCase.cs
new file mode 100644
index 0000000000..65f605e550
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/Schema/ResultCase.cs
@@ -0,0 +1,11 @@
+namespace Speckle.Automate.Sdk.Schema;
+
+public struct ResultCase
+{
+ public string Category { get; set; }
+ public string Level { get; set; }
+ public List ObjectIds { get; set; }
+ public string? Message { get; set; }
+ public Dictionary? Metadata { get; set; }
+ public Dictionary? VisualOverrides { get; set; }
+}
diff --git a/Automate/Speckle.Automate.Sdk/Schema/UploadResult.cs b/Automate/Speckle.Automate.Sdk/Schema/UploadResult.cs
new file mode 100644
index 0000000000..0fce6d4919
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/Schema/UploadResult.cs
@@ -0,0 +1,8 @@
+namespace Speckle.Automate.Sdk.Schema;
+
+public struct UploadResult
+{
+ public string BlobId { get; set; }
+ public string FileName { get; set; }
+ public int UploadStatus { get; set; }
+}
diff --git a/Automate/Speckle.Automate.Sdk/Speckle.Automate.Sdk.csproj b/Automate/Speckle.Automate.Sdk/Speckle.Automate.Sdk.csproj
new file mode 100644
index 0000000000..c3f4dc5858
--- /dev/null
+++ b/Automate/Speckle.Automate.Sdk/Speckle.Automate.Sdk.csproj
@@ -0,0 +1,31 @@
+
+
+
+ netstandard2.0
+ enable
+ enable
+ true
+ Speckle.Automate.Sdk
+ Speckle.Automate.Sdk
+ Speckle Automate SDK
+ $(PackageTags) speckle automation
+ Speckle.Automate.Sdk
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/GetAutomationStatus.cs b/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/GetAutomationStatus.cs
new file mode 100644
index 0000000000..43b8833103
--- /dev/null
+++ b/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/GetAutomationStatus.cs
@@ -0,0 +1,86 @@
+using GraphQL;
+using Speckle.Core.Api;
+
+namespace Speckle.Automate.Sdk.Tests.Integration;
+
+public class FunctionRun
+{
+ public string StatusMessage { get; set; }
+}
+
+public class AutomationRun
+{
+ public string Status { get; set; }
+ public IList FunctionRuns { get; set; }
+}
+
+public class AutomationStatus
+{
+ public string Status { get; set; }
+ public IList AutomationRuns { get; set; }
+}
+
+public class ModelAutomationStatus
+{
+ public AutomationStatus AutomationStatus { get; set; }
+}
+
+public class ProjectAutomationStatus
+{
+ public ModelAutomationStatus Model { get; set; }
+}
+
+public class AutomationStatusResponseModel
+{
+ public ProjectAutomationStatus Project { get; set; }
+}
+
+public static class AutomationStatusOperations
+{
+ public static async Task Get(string projectId, string modelId, Client speckleClient)
+ {
+ GraphQLRequest query =
+ new(
+ """
+ query AutomationRuns(
+ $projectId: String!
+ $modelId: String!
+ )
+ {
+ project(id: $projectId) {
+ model(id: $modelId) {
+ automationStatus {
+ id
+ status
+ statusMessage
+ automationRuns {
+ id
+ automationId
+ versionId
+ createdAt
+ updatedAt
+ status
+ functionRuns {
+ id
+ functionId
+ elapsed
+ status
+ contextView
+ statusMessage
+ results
+ resultVersions {
+ id
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """,
+ variables: new { projectId, modelId, }
+ );
+ var response = await speckleClient.ExecuteGraphQLRequest(query);
+ return response.Project.Model.AutomationStatus;
+ }
+}
diff --git a/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/GlobalUsings.cs b/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/GlobalUsings.cs
new file mode 100644
index 0000000000..324456763a
--- /dev/null
+++ b/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/GlobalUsings.cs
@@ -0,0 +1 @@
+global using NUnit.Framework;
diff --git a/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/Speckle.Automate.Sdk.Tests.Integration.csproj b/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/Speckle.Automate.Sdk.Tests.Integration.csproj
new file mode 100644
index 0000000000..6cbda57ccc
--- /dev/null
+++ b/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/Speckle.Automate.Sdk.Tests.Integration.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+ false
+ true
+ $(NoWarn);CA2007
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/SpeckleAutomate.cs b/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/SpeckleAutomate.cs
new file mode 100644
index 0000000000..4960258f35
--- /dev/null
+++ b/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/SpeckleAutomate.cs
@@ -0,0 +1,234 @@
+using Speckle.Automate.Sdk.Schema;
+using Speckle.Core.Api;
+using Speckle.Core.Credentials;
+using Speckle.Core.Models;
+using Speckle.Core.Transports;
+using TestsIntegration;
+using Utils = Speckle.Automate.Sdk.Tests.Integration.TestAutomateUtils;
+
+namespace Speckle.Automate.Sdk.Tests.Integration;
+
+[TestFixture]
+public sealed class AutomationContextTest : IDisposable
+{
+ private async Task AutomationRunData(Base testObject)
+ {
+ string projectId = await client.StreamCreate(new() { name = "Automate function e2e test" });
+ const string branchName = "main";
+
+ Branch model = await client.BranchGet(projectId, branchName, 1);
+ string modelId = model.id;
+
+ string rootObjId = await Operations.Send(
+ testObject,
+ new List { new ServerTransport(client.Account, projectId) }
+ );
+
+ string versionId = await client.CommitCreate(
+ new()
+ {
+ streamId = projectId,
+ objectId = rootObjId,
+ branchName = model.name
+ }
+ );
+
+ var automationName = Utils.RandomString(10);
+ var automationId = Utils.RandomString(10);
+ var automationRevisionId = Utils.RandomString(10);
+
+ await Utils.RegisterNewAutomation(projectId, modelId, client, automationId, automationName, automationRevisionId);
+
+ var automationRunId = Utils.RandomString(10);
+ var functionId = Utils.RandomString(10);
+ var functionName = "Automation name " + Utils.RandomString(10);
+ var functionRelease = Utils.RandomString(10);
+
+ return new AutomationRunData
+ {
+ ProjectId = projectId,
+ ModelId = modelId,
+ BranchName = branchName,
+ VersionId = versionId,
+ SpeckleServerUrl = client.ServerUrl,
+ AutomationId = automationId,
+ AutomationRevisionId = automationRevisionId,
+ AutomationRunId = automationRunId,
+ FunctionId = functionId,
+ FunctionName = functionName,
+ FunctionRelease = functionRelease,
+ };
+ }
+
+ private Client client;
+ private Account account;
+
+ [OneTimeSetUp]
+ public async Task Setup()
+ {
+ account = await Fixtures.SeedUser().ConfigureAwait(false);
+ client = new Client(account);
+ }
+
+ [Test]
+ public async Task TestFunctionRun()
+ {
+ var automationRunData = await AutomationRunData(Utils.TestObject());
+ var automationContext = await AutomationRunner.RunFunction(
+ TestAutomateFunction.Run,
+ automationRunData,
+ account.token,
+ new TestFunctionInputs { ForbiddenSpeckleType = "Base" }
+ );
+
+ Assert.That(automationContext.RunStatus, Is.EqualTo("FAILED"));
+
+ var status = await AutomationStatusOperations.Get(
+ automationRunData.ProjectId,
+ automationRunData.ModelId,
+ automationContext.SpeckleClient
+ );
+
+ Assert.That(status.Status, Is.EqualTo(automationContext.RunStatus));
+ var statusMessage = status.AutomationRuns[0].FunctionRuns[0].StatusMessage;
+
+ Assert.That(statusMessage, Is.EqualTo(automationContext.AutomationResult.StatusMessage));
+ }
+
+ [Test]
+ public async Task TestFileUploads()
+ {
+ var automationRunData = await AutomationRunData(Utils.TestObject());
+ var automationContext = await AutomationContext.Initialize(automationRunData, account.token);
+
+ string filePath = $"./{Utils.RandomString(10)}";
+ await File.WriteAllTextAsync(filePath, "foobar");
+ try
+ {
+ await automationContext.StoreFileResult(filePath);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ throw;
+ }
+
+ File.Delete(filePath);
+ Assert.That(automationContext.AutomationResult.Blobs, Has.Count.EqualTo(1));
+ }
+
+ [Test]
+ public async Task TestCreateVersionInProject()
+ {
+ var automationRunData = await AutomationRunData(Utils.TestObject());
+ var automationContext = await AutomationContext.Initialize(automationRunData, account.token);
+
+ const string branchName = "test-branch";
+ const string commitMsg = "automation test";
+
+ await automationContext.CreateNewVersionInProject(Utils.TestObject(), branchName, commitMsg);
+
+ var branch = await automationContext.SpeckleClient
+ .BranchGet(automationRunData.ProjectId, branchName, 1)
+ .ConfigureAwait(false);
+
+ Assert.NotNull(branch);
+ Assert.That(branch.name, Is.EqualTo(branchName));
+ Assert.That(branch.commits.items[0].message, Is.EqualTo(commitMsg));
+ }
+
+ [Test]
+ public async Task TestCreateVersionInProject_ThrowsErrorForSameModel()
+ {
+ var automationRunData = await AutomationRunData(Utils.TestObject());
+ var automationContext = await AutomationContext.Initialize(automationRunData, account.token);
+
+ var branchName = automationRunData.BranchName;
+ const string commitMsg = "automation test";
+
+ Assert.ThrowsAsync(async () =>
+ {
+ await automationContext.CreateNewVersionInProject(Utils.TestObject(), branchName, commitMsg);
+ });
+ }
+
+ [Test]
+ public async Task TestSetContextView()
+ {
+ var automationRunData = await AutomationRunData(Utils.TestObject());
+ var automationContext = await AutomationContext.Initialize(automationRunData, account.token);
+
+ automationContext.SetContextView();
+
+ Assert.That(automationContext.AutomationResult.ResultView, Is.Not.Null);
+ string originModelView = $"{automationRunData.ModelId}@{automationRunData.VersionId}";
+ Assert.That(automationContext.AutomationResult.ResultView.EndsWith($"models/{originModelView}"), Is.True);
+
+ await automationContext.ReportRunStatus();
+ var dummyContext = "foo@bar";
+
+ automationContext.AutomationResult.ResultView = null;
+ automationContext.SetContextView(new List { dummyContext }, true);
+
+ Assert.That(automationContext.AutomationResult.ResultView, Is.Not.Null);
+ Assert.That(
+ automationContext.AutomationResult.ResultView.EndsWith($"models/{originModelView},{dummyContext}"),
+ Is.True
+ );
+
+ await automationContext.ReportRunStatus();
+
+ automationContext.AutomationResult.ResultView = null;
+ automationContext.SetContextView(new List { dummyContext }, false);
+
+ Assert.That(automationContext.AutomationResult.ResultView, Is.Not.Null);
+ Assert.That(automationContext.AutomationResult.ResultView.EndsWith($"models/{dummyContext}"), Is.True);
+
+ await automationContext.ReportRunStatus();
+
+ automationContext.AutomationResult.ResultView = null;
+
+ Assert.Throws(() =>
+ {
+ automationContext.SetContextView(null, false);
+ });
+
+ await automationContext.ReportRunStatus();
+ }
+
+ [Test]
+ public async Task TestReportRunStatus_Succeeded()
+ {
+ var automationRunData = await AutomationRunData(Utils.TestObject());
+ var automationContext = await AutomationContext.Initialize(automationRunData, account.token);
+
+ Assert.That(automationContext.RunStatus, Is.EqualTo(AutomationStatusMapping.Get(Schema.AutomationStatus.Running)));
+
+ automationContext.MarkRunSuccess("This is a success message");
+
+ Assert.That(
+ automationContext.RunStatus,
+ Is.EqualTo(AutomationStatusMapping.Get(Schema.AutomationStatus.Succeeded))
+ );
+ }
+
+ [Test]
+ public async Task TestReportRunStatus_Failed()
+ {
+ var automationRunData = await AutomationRunData(Utils.TestObject());
+ var automationContext = await AutomationContext.Initialize(automationRunData, account.token);
+
+ Assert.That(automationContext.RunStatus, Is.EqualTo(AutomationStatusMapping.Get(Schema.AutomationStatus.Running)));
+
+ var message = "This is a failure message";
+ automationContext.MarkRunFailed(message);
+
+ Assert.That(automationContext.RunStatus, Is.EqualTo(AutomationStatusMapping.Get(Schema.AutomationStatus.Failed)));
+ Assert.That(automationContext.StatusMessage, Is.EqualTo(message));
+ }
+
+ public void Dispose()
+ {
+ client.Dispose();
+ }
+}
diff --git a/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/TestAutomateFunction.cs b/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/TestAutomateFunction.cs
new file mode 100644
index 0000000000..e64be99771
--- /dev/null
+++ b/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/TestAutomateFunction.cs
@@ -0,0 +1,43 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Speckle.Automate.Sdk.Tests.Integration;
+
+public struct TestFunctionInputs
+{
+ [Required]
+ public string ForbiddenSpeckleType { get; set; }
+}
+
+public static class TestAutomateFunction
+{
+ public static async Task Run(AutomationContext automateContext, TestFunctionInputs testFunctionInputs)
+ {
+ var versionRootObject = await automateContext.ReceiveVersion();
+
+ int count = 0;
+ if (versionRootObject.speckle_type == testFunctionInputs.ForbiddenSpeckleType)
+ {
+ if (versionRootObject.id is null)
+ throw new InvalidOperationException("Cannot operate on objects without their ids");
+
+ automateContext.AttachErrorToObjects(
+ "",
+ new[] { versionRootObject.id },
+ $"This project should not contain the type: {testFunctionInputs.ForbiddenSpeckleType} "
+ );
+ count += 1;
+ }
+
+ if (count > 0)
+ {
+ automateContext.MarkRunFailed(
+ "Automation failed: "
+ + $"Found {count} object that have a forbidden speckle type: {testFunctionInputs.ForbiddenSpeckleType}"
+ );
+ }
+ else
+ {
+ automateContext.MarkRunSuccess("No forbidden types found.");
+ }
+ }
+}
diff --git a/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/TestAutomateUtils.cs b/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/TestAutomateUtils.cs
new file mode 100644
index 0000000000..80fb5326cc
--- /dev/null
+++ b/Automate/Tests/Speckle.Automate.Sdk.Tests.Integration/TestAutomateUtils.cs
@@ -0,0 +1,69 @@
+using System.Diagnostics.CodeAnalysis;
+using GraphQL;
+using Speckle.Core.Api;
+using Speckle.Core.Models;
+
+namespace Speckle.Automate.Sdk.Tests.Integration;
+
+public static class TestAutomateUtils
+{
+ [SuppressMessage("Security", "CA5394:Do not use insecure randomness")]
+ public static string RandomString(int length)
+ {
+ Random rand = new();
+ const string pool = "abcdefghijklmnopqrstuvwxyz0123456789";
+ var chars = Enumerable.Range(0, length).Select(_ => pool[rand.Next(0, pool.Length)]);
+ return new string(chars.ToArray());
+ }
+
+ public static Base TestObject()
+ {
+ Base rootObject = new() { ["foo"] = "bar" };
+ return rootObject;
+ }
+
+ public static async Task RegisterNewAutomation(
+ string projectId,
+ string modelId,
+ Client speckleClient,
+ string automationId,
+ string automationName,
+ string automationRevisionId
+ )
+ {
+ GraphQLRequest query =
+ new(
+ query: """
+ mutation CreateAutomation(
+ $projectId: String!
+ $modelId: String!
+ $automationName: String!
+ $automationId: String!
+ $automationRevisionId: String!
+ ) {
+ automationMutations {
+ create(
+ input: {
+ projectId: $projectId
+ modelId: $modelId
+ automationName: $automationName
+ automationId: $automationId
+ automationRevisionId: $automationRevisionId
+ }
+ )
+ }
+ }
+ """,
+ variables: new
+ {
+ projectId,
+ modelId,
+ automationName,
+ automationId,
+ automationRevisionId,
+ }
+ );
+
+ await speckleClient.ExecuteGraphQLRequest