diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml
index 760b28067..6f058b390 100644
--- a/.github/workflows/packages.yml
+++ b/.github/workflows/packages.yml
@@ -70,6 +70,17 @@ jobs:
dotnet test "$project" --configuration Release --no-build --logger "GitHubActions;report-warnings=false" /p:CollectCoverage=true
done
+ - name: Install ReportGenerator
+ run: dotnet tool install -g dotnet-reportgenerator-globaltool
+
+ - name: Generate HTML coverage report
+ run: |
+ reportgenerator \
+ "-reports:./artifacts/coverage/**/coverage.cobertura.xml" \
+ "-targetdir:./artifacts/coverage-report" \
+ "-reporttypes:Html;Cobertura;TextSummary" \
+ "-verbosity:Info"
+
- name: Upload coverage reports
if: always()
uses: actions/upload-artifact@v4
@@ -77,6 +88,12 @@ jobs:
name: coverage-reports
path: artifacts/coverage
+ - name: Upload Pages artifact
+ if: always() && github.ref == 'refs/heads/main'
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: './artifacts/coverage-report'
+
build:
name: Build packages
needs: test
@@ -168,3 +185,19 @@ jobs:
- name: Publish to nuget.org
run: dotnet nuget push *.nupkg -k ${{ secrets.NUGET_API_KEY }} -s ${{ env.nuget_feed_source }} --skip-duplicate
+
+ deploy_coverage:
+ name: Deploy coverage to GitHub Pages
+ needs: test
+ runs-on: ubuntu-latest
+ if: github.ref == 'refs/heads/main'
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index 68a650b2a..34040f1bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -80,3 +80,7 @@ unlist.sh
/docker/data/
/docker/azurite-data/
docker/docker-compose-datadog.yml
+
+# Test results
+TestResults/
+artifacts/
diff --git a/tempvalidator/Program.cs b/tempvalidator/Program.cs
deleted file mode 100644
index 3751555cb..000000000
--- a/tempvalidator/Program.cs
+++ /dev/null
@@ -1,2 +0,0 @@
-// See https://aka.ms/new-console-template for more information
-Console.WriteLine("Hello, World!");
diff --git a/tempvalidator/tempvalidator.csproj b/tempvalidator/tempvalidator.csproj
deleted file mode 100644
index 2150e3797..000000000
--- a/tempvalidator/tempvalidator.csproj
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
- Exe
- net8.0
- enable
- enable
-
-
-
diff --git a/test/integration/Elsa.Activities.IntegrationTests/Primitives/FinishTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Primitives/FinishTests.cs
new file mode 100644
index 000000000..398e29819
--- /dev/null
+++ b/test/integration/Elsa.Activities.IntegrationTests/Primitives/FinishTests.cs
@@ -0,0 +1,36 @@
+using Elsa.Activities.IntegrationTests.Primitives.Workflows;
+using Elsa.Testing.Shared;
+using Elsa.Workflows;
+using Elsa.Workflows.Activities;
+using Elsa.Workflows.State;
+using Xunit.Abstractions;
+
+namespace Elsa.Activities.IntegrationTests.Primitives;
+
+///
+/// Integration tests for the activity.
+///
+public class FinishTests(ITestOutputHelper testOutputHelper)
+{
+ private readonly WorkflowTestFixture _fixture = new WorkflowTestFixture(testOutputHelper)
+ .AddWorkflow();
+
+ [Fact(DisplayName = "Finish terminates workflow and prevents subsequent activities from executing")]
+ public async Task Finish_TerminatesWorkflowAndPreventsSubsequentActivities()
+ {
+ // Act
+ var workflowState = await _fixture.RunWorkflowAsync(FinishInSequenceWorkflow.DefinitionId);
+
+ // Assert
+ AssertWorkflowFinished(workflowState);
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
+ Assert.Contains("Before Finish", lines);
+ Assert.DoesNotContain("After Finish", lines);
+ }
+
+ private static void AssertWorkflowFinished(WorkflowState workflowState)
+ {
+ Assert.Equal(WorkflowStatus.Finished, workflowState.Status);
+ Assert.Equal(WorkflowSubStatus.Finished, workflowState.SubStatus);
+ }
+}
diff --git a/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FinishInSequenceWorkflow.cs b/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FinishInSequenceWorkflow.cs
new file mode 100644
index 000000000..4963327ff
--- /dev/null
+++ b/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FinishInSequenceWorkflow.cs
@@ -0,0 +1,24 @@
+using Elsa.Workflows;
+using Elsa.Workflows.Activities;
+using System;
+
+namespace Elsa.Activities.IntegrationTests.Primitives.Workflows;
+
+public class FinishInSequenceWorkflow : WorkflowBase
+{
+ public static readonly string DefinitionId = Guid.NewGuid().ToString();
+
+ protected override void Build(IWorkflowBuilder workflow)
+ {
+ workflow.WithDefinitionId(DefinitionId);
+ workflow.Root = new Sequence
+ {
+ Activities =
+ {
+ new WriteLine("Before Finish"),
+ new Finish(),
+ new WriteLine("After Finish")
+ }
+ };
+ }
+}
diff --git a/test/unit/Elsa.Activities.UnitTests/Primitives/FinishTests.cs b/test/unit/Elsa.Activities.UnitTests/Primitives/FinishTests.cs
new file mode 100644
index 000000000..8e9fe5da5
--- /dev/null
+++ b/test/unit/Elsa.Activities.UnitTests/Primitives/FinishTests.cs
@@ -0,0 +1,51 @@
+using Elsa.Testing.Shared;
+using Elsa.Workflows;
+
+namespace Elsa.Activities.UnitTests.Primitives;
+
+///
+/// Unit tests for the activity.
+///
+public class FinishTests
+{
+ [Fact(DisplayName = "Finish implements ITerminalNode interface")]
+ public void Finish_ImplementsITerminalNode()
+ {
+ // Arrange
+ var finish = new Finish();
+
+ // Assert
+ Assert.IsAssignableFrom(finish);
+ }
+
+ [Fact(DisplayName = "Finish completes successfully")]
+ public async Task Should_Complete_Successfully()
+ {
+ // Arrange
+ var finish = new Finish();
+
+ // Act
+ var context = await ExecuteAsync(finish);
+
+ // Assert
+ Assert.Equal(ActivityStatus.Completed, context.Status);
+ }
+
+ [Fact(DisplayName = "Finish transitions workflow to Finished substatus")]
+ public async Task Should_Transition_Workflow_To_Finished()
+ {
+ // Arrange
+ var finish = new Finish();
+
+ // Act
+ var context = await ExecuteAsync(finish);
+
+ // Assert
+ Assert.Equal(WorkflowSubStatus.Finished, context.WorkflowExecutionContext.SubStatus);
+ }
+
+ private static async Task ExecuteAsync(IActivity activity)
+ {
+ return await new ActivityTestFixture(activity).ExecuteAsync();
+ }
+}