Add WorkflowStateCommitted notification support and update state handling logic
- Introduced `WorkflowStateCommitted` notification to encapsulate workflow execution context, state, and instance details. - Updated `DefaultCommitStateHandler` to publish `WorkflowStateCommitted` via `IMediator`. - Adjusted `DispatchWorkflowExtensions` to use `WorkflowStateCommitted` for workflow completion. Updates KubernetesClient and Microsoft packages (#6917) * Remove Proto.Cluster.Kubernetes dependency due to vulnerability - Temporarily removed `Proto.Cluster.Kubernetes` package and provider integration because of a vulnerability in its dependency (https://avd.aquasec.com/nvd/2025/cve-2025-9708). - Adjusted related cluster provider and remote configuration logic. - Updated `PortAttribute` default parameter for clarity. * Revert "Remove Proto.Cluster.Kubernetes dependency due to vulnerability" This reverts commit 0720d970968e4f7338825407258b34ddffb1d2a4. * Add KubernetesClient package and update MicrosoftVersion to 9.0.9 - Added `KubernetesClient` package to the project dependencies. - Updated `MicrosoftVersion` to `9.0.9` in `Directory.Packages.props`. Update Polly packages - Bump Polly and Polly.Extensions package versions to 8.6.3. Update `Microsoft.AspNetCore.Authorization` to use `MicrosoftVersion` property Ensure Docker images ship CA trust and add TLS smoke tests (#6918) Remove TlsSmoke project and related solution references - Deleted `TlsSmoke` project files (`Program.cs` and `TlsSmoke.csproj`). - Removed `TlsSmoke` project reference from the solution file (`Elsa.sln`). Add comprehensive Copilot coding agent instructions for repository onboarding (#6920) * Initial plan * Add comprehensive .github/copilot-instructions.md with validated build instructions Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> Add ForEach tests, introduce asynchronous workflow runner and enhance workflow events. (#6926) * Introduce asynchronous workflow runner and enhance workflow events. - Added `AsyncWorkflowRunner` to enable asynchronous workflow execution and result tracking. - Introduced new event arguments, such as `ActivityExecutedEventArgs` and `WorkflowStateCommittedEventArgs`. - Expanded `WorkflowEvents` class to include `ActivityExecuted`, `ActivityExecutedLogUpdated`, and `WorkflowStateCommitted` events. - Refactored event arguments into the `Elsa.Testing.Shared.EventArgs` namespace. - Enhanced tests with `AsyncWorkflowRunner` and new event-driven workflow scenarios. * Refactor event argument classes to unify namespace and simplify inheritance * Add shared component DotSettings file to support namespace exclusions Refactor `WaitAsync` call in `DispatchWorkflowsTests` to remove unnecessary generic type.
This commit is contained in:
parent
4f4263e544
commit
f24b4394cf
230
.github/copilot-instructions.md
vendored
Normal file
230
.github/copilot-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
# Copilot Coding Agent Instructions for Elsa Workflows
|
||||
|
||||
## Repository Overview
|
||||
|
||||
**Elsa Workflows** is a powerful .NET workflow library that enables workflow execution within any .NET application. This is version 3.0, supporting .NET 9.0 and providing both a visual designer and programmatic workflow definition capabilities.
|
||||
|
||||
### Key Statistics
|
||||
- **Language**: C# (.NET 9.0)
|
||||
- **Architecture**: Modular library with 104+ projects
|
||||
- **Code Size**: ~3,500 C# files across modules
|
||||
- **License**: MIT
|
||||
- **Build System**: NUKE build automation
|
||||
- **Target Frameworks**: .NET 9.0 (primary)
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
src/
|
||||
├── apps/ # Reference applications (5 projects)
|
||||
│ ├── Elsa.Server.Web # Workflow server only
|
||||
│ ├── Elsa.ServerAndStudio.Web # Combined server + studio
|
||||
│ ├── Elsa.Studio.Web # Studio web interface
|
||||
│ ├── ElsaStudioWebAssembly # Studio WebAssembly app
|
||||
│ └── Elsa.Server.LoadBalancer # Load balancer
|
||||
├── common/ # Shared libraries (8 projects)
|
||||
├── modules/ # Core functionality modules (70+ projects)
|
||||
│ ├── Elsa.Workflows.Core # Core workflow engine
|
||||
│ ├── Elsa.Workflows.Runtime # Runtime execution
|
||||
│ ├── Elsa.Workflows.Api # REST API
|
||||
│ ├── Elsa.Http # HTTP activities
|
||||
│ ├── Elsa.Email # Email activities
|
||||
│ └── [many others] # Database, messaging, etc.
|
||||
└── clients/ # API clients
|
||||
test/
|
||||
├── unit/ # Unit tests
|
||||
├── integration/ # Integration tests
|
||||
├── component/ # Component tests
|
||||
└── performance/ # Performance tests
|
||||
build/ # NUKE build configuration
|
||||
docker/ # Docker configurations
|
||||
```
|
||||
|
||||
### Core Components
|
||||
- **Elsa.Workflows.Core**: Main workflow engine and activities
|
||||
- **Elsa.Workflows.Runtime**: Workflow execution runtime
|
||||
- **Elsa.Workflows.Api**: RESTful API for workflow management
|
||||
- **Elsa.Workflows.Management**: Workflow definition management
|
||||
- **Elsa modules**: Specialized functionality (HTTP, email, scheduling, etc.)
|
||||
|
||||
## Build Instructions
|
||||
|
||||
### Prerequisites
|
||||
- **.NET 9.0 SDK** (verified working version: 9.0.305)
|
||||
- **Build time**: Initial restore ~1-2 minutes, full compile ~5-10 minutes
|
||||
|
||||
### Critical Build Information
|
||||
|
||||
⚠️ **IMPORTANT**: The repository has external dependencies that may cause build failures:
|
||||
|
||||
1. **External NuGet Feeds**: Some projects depend on packages from:
|
||||
- `https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json` (Elsa Studio packages)
|
||||
- `https://f.feedz.io/sfmskywalker/webhooks-core/nuget/index.json` (Webhooks packages)
|
||||
|
||||
2. **Build Failure Workarounds**:
|
||||
- Studio apps (`Elsa.Studio.Web`, `ElsaStudioWebAssembly`, `Elsa.ServerAndStudio.Web`) depend on prebuilt studio packages that may not be accessible
|
||||
- Server app (`Elsa.Server.Web`) depends on WebhooksCore package that may not be accessible
|
||||
- Core workflow functionality can be built independently
|
||||
- Some test projects may fail due to missing external packages
|
||||
|
||||
### Build Commands
|
||||
|
||||
**Primary build script**: `./build.sh` (Linux/macOS) or `.\build.cmd` (Windows)
|
||||
|
||||
```bash
|
||||
# View available targets
|
||||
./build.sh --help
|
||||
|
||||
# Clean build artifacts
|
||||
./build.sh Clean
|
||||
|
||||
# Restore packages (may show warnings for inaccessible feeds)
|
||||
./build.sh Restore --ignore-failed-sources
|
||||
|
||||
# Compile core components (excludes studio apps)
|
||||
./build.sh Compile
|
||||
|
||||
# Run tests (limited due to external dependencies)
|
||||
./build.sh Test
|
||||
|
||||
# Create NuGet packages
|
||||
./build.sh Pack
|
||||
|
||||
# Full CI pipeline (compile, test, pack)
|
||||
./build.sh Compile Test Pack
|
||||
```
|
||||
|
||||
**Direct dotnet commands** for core components:
|
||||
```bash
|
||||
# Build specific core projects that don't require external packages
|
||||
dotnet restore src/modules/Elsa.Workflows.Core/ --ignore-failed-sources
|
||||
dotnet build src/modules/Elsa.Workflows.Core/ --no-restore
|
||||
dotnet restore src/modules/Elsa.Workflows.Runtime/ --ignore-failed-sources
|
||||
dotnet build src/modules/Elsa.Workflows.Runtime/ --no-restore
|
||||
|
||||
# Note: Server apps may fail due to WebhooksCore dependency
|
||||
# Build and test individual core modules
|
||||
find test/unit -name "*.csproj" | head -5 | xargs -I {} dotnet build {}
|
||||
```
|
||||
|
||||
### Expected Build Warnings
|
||||
- `NU1900`: Unable to load service index for external feeds (safe to ignore)
|
||||
- `NU1801`: Service index warnings for feedz.io sources (safe to ignore)
|
||||
- `NU1101`: Missing Elsa.Studio packages (blocks studio app builds)
|
||||
|
||||
### Successful Build Indicators
|
||||
- Core modules (Elsa.Workflows.Core, etc.) compile successfully
|
||||
- Server applications (Elsa.Server.Web) build without the studio UI
|
||||
- Most modules show "succeeded with X warning(s)" (warnings are acceptable)
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Structure
|
||||
- **Unit tests**: `test/unit/` - Fast, isolated tests
|
||||
- **Integration tests**: `test/integration/` - End-to-end scenarios
|
||||
- **Component tests**: `test/component/` - Feature testing
|
||||
- **Performance tests**: `test/performance/` - Benchmarks
|
||||
|
||||
### Running Tests
|
||||
```bash
|
||||
# Via NUKE build system
|
||||
./build.sh Test
|
||||
|
||||
# Direct dotnet test (for accessible projects)
|
||||
dotnet test test/unit/[specific-project]/
|
||||
dotnet test --no-build --no-restore [project-path]
|
||||
```
|
||||
|
||||
**Note**: Many tests may fail to run due to external package dependencies. Focus on core workflow engine tests that don't require studio packages.
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Standards
|
||||
- **Language version**: C# latest
|
||||
- **Target framework**: .NET 9.0
|
||||
- **Nullable reference types**: Enabled
|
||||
- **Implicit usings**: Enabled
|
||||
- **EditorConfig**: Configured (4-space indentation, CRLF line endings)
|
||||
|
||||
### Architecture Patterns
|
||||
- **Modular design**: Each feature area is a separate project/module
|
||||
- **Dependency injection**: Heavy use of Microsoft.Extensions.DependencyInjection
|
||||
- **Activity-based**: Workflows are built from composable activities
|
||||
- **Async/await**: Extensive use throughout for scalability
|
||||
|
||||
### Common Gotchas
|
||||
1. **External Dependencies**: Studio-related projects require external packages
|
||||
2. **NuGet Source Mapping**: Configured in NuGet.Config, restricts where packages can be sourced
|
||||
3. **Multiple Target Frameworks**: Some projects conditionally target different frameworks
|
||||
4. **Build Warnings**: Many NU1900/NU1801 warnings are expected and safe
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
### GitHub Actions Workflow
|
||||
- **Trigger**: Pull requests to `main` branch
|
||||
- **Runner**: ubuntu-latest
|
||||
- **.NET Version**: 9.x (latest)
|
||||
- **Commands**: `./build.cmd Compile Test Pack`
|
||||
- **File**: `.github/workflows/pr.yml` (auto-generated by NUKE)
|
||||
|
||||
### CI Pipeline Steps
|
||||
1. Checkout code
|
||||
2. Setup .NET 9.x SDK
|
||||
3. Execute: Compile → Test → Pack
|
||||
4. Expected warnings for external feed access
|
||||
5. Studio apps may be excluded from CI builds
|
||||
|
||||
## Key Configuration Files
|
||||
|
||||
- **Build**: `build/Build.cs` (NUKE build configuration)
|
||||
- **Dependencies**: `Directory.Packages.props` (central package management)
|
||||
- **Global settings**: `Directory.Build.props`
|
||||
- **NuGet**: `NuGet.Config` (package sources and mapping)
|
||||
- **Solution**: `Elsa.sln` (119 projects)
|
||||
- **Docker**: `docker/` directory with multiple Dockerfiles
|
||||
- **GitHub Actions**: `.github/workflows/` (auto-generated)
|
||||
|
||||
## Docker Support
|
||||
|
||||
Multiple Docker configurations available:
|
||||
- `ElsaServer.Dockerfile` - Server only
|
||||
- `ElsaServerAndStudio.Dockerfile` - Combined server + studio
|
||||
- `ElsaStudio.Dockerfile` - Studio only
|
||||
- Docker Compose configurations for development
|
||||
|
||||
## Quick Start for Development
|
||||
|
||||
1. **Clone and build core components**:
|
||||
```bash
|
||||
git clone [repo-url]
|
||||
cd elsa-core
|
||||
./build.sh Clean Restore --ignore-failed-sources
|
||||
```
|
||||
|
||||
2. **Work with core modules** (avoid studio dependencies):
|
||||
```bash
|
||||
cd src/modules/Elsa.Workflows.Core
|
||||
dotnet build
|
||||
dotnet test ../../test/unit/[related-tests]/
|
||||
```
|
||||
|
||||
3. **Work with core modules that don't require external dependencies**:
|
||||
```bash
|
||||
cd src/modules/Elsa.Workflows.Core
|
||||
dotnet restore --ignore-failed-sources
|
||||
dotnet build --no-restore
|
||||
```
|
||||
|
||||
## Important Notes for Coding Agents
|
||||
|
||||
1. **Always use `--ignore-failed-sources`** when restoring packages
|
||||
2. **Focus on core workflow functionality** rather than studio UI components
|
||||
3. **Studio apps require external packages** that may not be accessible
|
||||
4. **Build warnings are normal** - don't try to fix NU1900/NU1801 warnings
|
||||
5. **Test individual modules** rather than solution-wide tests when external deps fail
|
||||
6. **Use direct dotnet commands** for building specific components when NUKE fails
|
||||
7. **Check project references** before attempting builds - some projects have conditional references
|
||||
8. **Start with core modules** like `Elsa.Workflows.Core`, `Elsa.Workflows.Runtime` which are more likely to build successfully
|
||||
|
||||
Trust these instructions for build and development workflows. Only search for additional information if these instructions are incomplete or found to be incorrect.
|
||||
72
.github/workflows/docker-ca.yml
vendored
Normal file
72
.github/workflows/docker-ca.yml
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
name: Docker certificate smoke tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docker/**'
|
||||
- 'scripts/test-ca-trust.sh'
|
||||
- 'test/TlsSmoke/**'
|
||||
- '.github/workflows/docker-ca.yml'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docker/**'
|
||||
- 'scripts/test-ca-trust.sh'
|
||||
- 'test/TlsSmoke/**'
|
||||
- '.github/workflows/docker-ca.yml'
|
||||
|
||||
jobs:
|
||||
smoke:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: elsa-server
|
||||
dockerfile: docker/ElsaServer.Dockerfile
|
||||
image: elsa-server-smoke
|
||||
skip_otel: '0'
|
||||
- name: elsa-server-and-studio
|
||||
dockerfile: docker/ElsaServerAndStudio.Dockerfile
|
||||
image: elsa-server-and-studio-smoke
|
||||
skip_otel: '0'
|
||||
- name: elsa-studio
|
||||
dockerfile: docker/ElsaStudio.Dockerfile
|
||||
image: elsa-studio-smoke
|
||||
skip_otel: '0'
|
||||
- name: elsa-server-datadog
|
||||
dockerfile: docker/ElsaServer-Datadog.Dockerfile
|
||||
image: elsa-server-datadog-smoke
|
||||
skip_otel: '1'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Publish TLS smoke test app
|
||||
run: dotnet publish test/TlsSmoke/TlsSmoke.csproj -c Release -o artifacts/tls-smoke
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build ${{ matrix.name }} image
|
||||
run: docker build -f ${{ matrix.dockerfile }} -t ${{ matrix.image }} .
|
||||
|
||||
- name: Run TLS smoke tests
|
||||
env:
|
||||
SKIP_OTEL: ${{ matrix.skip_otel }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TLS_APP_DIR="${{ github.workspace }}/artifacts/tls-smoke"
|
||||
if [ "$SKIP_OTEL" = "1" ]; then
|
||||
scripts/test-ca-trust.sh "${{ matrix.image }}" "$TLS_APP_DIR" -e ELSA_SKIP_OTEL_AUTO=1
|
||||
else
|
||||
scripts/test-ca-trust.sh "${{ matrix.image }}" "$TLS_APP_DIR"
|
||||
fi
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ElsaStudioVersion>3.6.0-preview.1175</ElsaStudioVersion>
|
||||
<MicrosoftVersion>9.0.8</MicrosoftVersion>
|
||||
<MicrosoftVersion>9.0.9</MicrosoftVersion>
|
||||
<ResilienceVersion>9.7.0</ResilienceVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
|||
5
Elsa.sln
5
Elsa.sln
|
|
@ -59,14 +59,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E54
|
|||
docker\ElsaStudio.Dockerfile = docker\ElsaStudio.Dockerfile
|
||||
docker\init-db-postgres.sh = docker\init-db-postgres.sh
|
||||
docker\otel-collector-config.yaml = docker\otel-collector-config.yaml
|
||||
docker\docker-compose-datadog+otel-collector.yml = docker\docker-compose-datadog+otel-collector.yml
|
||||
docker\entrypoint.sh = docker\entrypoint.sh
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "unit", "unit", "{18453B51-25EB-4317-A4B3-B10518252E92}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "integration", "integration", "{1B8D5897-902E-4632-8698-E89CAF3DDF54}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
test\integration\Elsa.Logging.Core.LoggerSinkTests.cs = test\integration\Elsa.Logging.Core.LoggerSinkTests.cs
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "component", "component", "{08B41FFA-CEE3-46A7-B5C0-3EB65D37A16C}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
|
|
|
|||
26
README.md
26
README.md
|
|
@ -39,6 +39,32 @@ By default, you can access http://localhost:13000 and log in with:
|
|||
Password: password
|
||||
```
|
||||
|
||||
### TLS and custom certificate authorities
|
||||
|
||||
All Elsa Docker images now ship with the operating system's certificate authority bundle baked in at build time. This means you can call public HTTPS endpoints such as `https://example.com` without any additional configuration.
|
||||
|
||||
If you need to trust a private or corporate CA, mount the certificate bundle into the container and reference it via `EXTRA_CA_CERT`:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-v /path/to/company-ca.crt:/certs/company-ca.crt:ro \
|
||||
-e EXTRA_CA_CERT=/certs/company-ca.crt \
|
||||
elsaworkflows/elsa-server-and-studio-v3:latest
|
||||
```
|
||||
|
||||
On startup, the container copies the certificate into `/usr/local/share/ca-certificates` and runs `update-ca-certificates`, making the trust available to .NET, OpenSSL, curl, and other system components. Multiple certificates can be provided by pointing `EXTRA_CA_CERT` at a directory containing `.crt` or `.pem` files.
|
||||
|
||||
In highly restricted environments where you cannot modify the system trust store, you can instead rely on the standard `SSL_CERT_FILE` or `SSL_CERT_DIR` environment variables:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-v /path/to/company-ca-bundle.pem:/certs/custom.pem:ro \
|
||||
-e SSL_CERT_FILE=/certs/custom.pem \
|
||||
elsaworkflows/elsa-server-and-studio-v3:latest
|
||||
```
|
||||
|
||||
> ℹ️ Installing the CA bundle adds roughly 300KB to the Debian-based images. No package managers run at container startup; all trust updates happen immutably at build time or via the mounted certificates shown above.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Documentation](#documentation)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Version: 1
|
||||
# Description: Dockerfile for building and running Elsa Server
|
||||
# Description: Dockerfile for building and running Elsa Server with Datadog and OpenTelemetry auto-instrumentation
|
||||
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim AS build
|
||||
WORKDIR /source
|
||||
|
|
@ -22,36 +22,40 @@ FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim AS base
|
|||
WORKDIR /app
|
||||
COPY --from=build /app/publish ./
|
||||
|
||||
# Install Python 3.11
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3.11 \
|
||||
python3.11-dev \
|
||||
libpython3.11 \
|
||||
python3-pip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
# Install runtime dependencies, including CA certificates.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
libpython3.11 \
|
||||
python3.11 \
|
||||
python3.11-dev \
|
||||
python3-pip \
|
||||
unzip \
|
||||
wget \
|
||||
&& update-ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Set PYTHONNET_PYDLL environment variable
|
||||
ENV PYTHONNET_PYDLL=/usr/lib/aarch64-linux-gnu/libpython3.11.so
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && apt-get install -y wget unzip curl
|
||||
|
||||
# Set environment variables for OpenTelemetry Auto-Instrumentation
|
||||
ENV OTEL_DOTNET_AUTO_HOME=/otel
|
||||
ENV OTEL_LOG_LEVEL="debug"
|
||||
ENV OTEL_DOTNET_AUTO_HOME=/otel \
|
||||
OTEL_LOG_LEVEL="debug"
|
||||
|
||||
# Download and extract OpenTelemetry Auto-Instrumentation
|
||||
ARG OTEL_VERSION=1.7.0
|
||||
RUN mkdir /otel
|
||||
RUN curl -L -o /otel/otel-dotnet-install.sh https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/download/v${OTEL_VERSION}/otel-dotnet-auto-install.sh
|
||||
RUN chmod +x /otel/otel-dotnet-install.sh
|
||||
RUN /bin/bash /otel/otel-dotnet-install.sh
|
||||
|
||||
# Provide necessary permissions for the script to execute
|
||||
RUN chmod +x /otel/instrument.sh
|
||||
RUN mkdir -p /otel \
|
||||
&& curl -L -o /otel/otel-dotnet-install.sh "https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/download/v${OTEL_VERSION}/otel-dotnet-auto-install.sh" \
|
||||
&& chmod +x /otel/otel-dotnet-install.sh \
|
||||
&& /bin/bash /otel/otel-dotnet-install.sh \
|
||||
&& chmod +x /otel/instrument.sh
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
EXPOSE 443/tcp
|
||||
|
||||
# Instrument the application and start it
|
||||
ENTRYPOINT ["/bin/bash", "-c", "source /otel/instrument.sh && dotnet Elsa.Server.Web.dll"]
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["dotnet", "Elsa.Server.Web.dll"]
|
||||
|
|
|
|||
|
|
@ -19,17 +19,24 @@ FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim AS base
|
|||
WORKDIR /app
|
||||
COPY --from=build /app/publish ./
|
||||
|
||||
# Install Python 3.11
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3.11 \
|
||||
python3.11-dev \
|
||||
libpython3.11 \
|
||||
python3-pip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
# Install runtime dependencies, including CA certificates.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
libpython3.11 \
|
||||
python3.11 \
|
||||
python3.11-dev \
|
||||
python3-pip \
|
||||
&& update-ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Set PYTHONNET_PYDLL environment variable
|
||||
ENV PYTHONNET_PYDLL=/usr/lib/aarch64-linux-gnu/libpython3.11.so
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
EXPOSE 443/tcp
|
||||
ENTRYPOINT ["dotnet", "Elsa.Server.Web.dll"]
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["dotnet", "Elsa.Server.Web.dll"]
|
||||
|
|
|
|||
|
|
@ -20,17 +20,24 @@ FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim AS base
|
|||
WORKDIR /app
|
||||
COPY --from=build /app/publish ./
|
||||
|
||||
# Install Python 3.11
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3.11 \
|
||||
python3.11-dev \
|
||||
libpython3.11 \
|
||||
python3-pip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
# Install runtime dependencies, including CA certificates.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
libpython3.11 \
|
||||
python3.11 \
|
||||
python3.11-dev \
|
||||
python3-pip \
|
||||
&& update-ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Set PYTHONNET_PYDLL environment variable
|
||||
ENV PYTHONNET_PYDLL=/usr/lib/aarch64-linux-gnu/libpython3.11.so
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
EXPOSE 443/tcp
|
||||
ENTRYPOINT ["dotnet", "Elsa.ServerAndStudio.Web.dll"]
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["dotnet", "Elsa.ServerAndStudio.Web.dll"]
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@ FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim AS base
|
|||
WORKDIR /app
|
||||
COPY --from=build /app/publish ./
|
||||
|
||||
# Install CA certificates so HTTPS works out of the box.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||
&& update-ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
EXPOSE 443/tcp
|
||||
ENTRYPOINT ["dotnet", "Elsa.Studio.Web.dll"]
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["dotnet", "Elsa.Studio.Web.dll"]
|
||||
|
|
|
|||
103
docker/entrypoint.sh
Executable file
103
docker/entrypoint.sh
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
log() {
|
||||
printf '%s\n' "$*" >&2
|
||||
}
|
||||
|
||||
normalise_dest_name() {
|
||||
name=$(basename "$1")
|
||||
case "$name" in
|
||||
*.crt|*.pem) printf '%s\n' "$name" ;;
|
||||
*) printf '%s.crt\n' "$name" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_extra_certificates() {
|
||||
cert_source="$1"
|
||||
if [ ! -e "$cert_source" ]; then
|
||||
log "EXTRA_CA_CERT path '$cert_source' does not exist; skipping installation."
|
||||
return
|
||||
fi
|
||||
|
||||
target_root="/usr/local/share/ca-certificates/extra"
|
||||
mkdir -p "$target_root"
|
||||
rm -f "$target_root"/* 2>/dev/null || true
|
||||
|
||||
copied=0
|
||||
if [ -f "$cert_source" ]; then
|
||||
dest_name=$(normalise_dest_name "$cert_source")
|
||||
cp "$cert_source" "$target_root/$dest_name"
|
||||
copied=1
|
||||
elif [ -d "$cert_source" ]; then
|
||||
for file in "$cert_source"/*.crt "$cert_source"/*.pem; do
|
||||
[ -f "$file" ] || continue
|
||||
dest_name=$(normalise_dest_name "$file")
|
||||
cp "$file" "$target_root/$dest_name"
|
||||
copied=1
|
||||
done
|
||||
else
|
||||
log "EXTRA_CA_CERT path '$cert_source' is neither a file nor a directory; skipping installation."
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$copied" -eq 0 ]; then
|
||||
log "No certificate files found at '$cert_source'; skipping installation."
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v update-ca-certificates >/dev/null 2>&1; then
|
||||
if ! update-ca-certificates >/dev/null 2>&1; then
|
||||
update-ca-certificates
|
||||
fi
|
||||
log "Installed custom certificate(s) from '$cert_source'."
|
||||
elif command -v trust >/dev/null 2>&1; then
|
||||
# Shellcheck disable because we intentionally glob.
|
||||
# shellcheck disable=SC2086
|
||||
for cert in "$target_root"/*.crt; do
|
||||
[ -f "$cert" ] || continue
|
||||
trust anchor "$cert"
|
||||
done
|
||||
log "Installed custom certificate(s) using 'trust' utility."
|
||||
else
|
||||
log "No known certificate installation tool found; custom CA may not be applied."
|
||||
fi
|
||||
}
|
||||
|
||||
maybe_instrument_with_otel() {
|
||||
if [ "${ELSA_SKIP_OTEL_AUTO:-0}" = "1" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -z "${OTEL_DOTNET_AUTO_HOME:-}" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
instrument_script="${OTEL_DOTNET_AUTO_HOME%/}/instrument.sh"
|
||||
if [ ! -f "$instrument_script" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if ! command -v bash >/dev/null 2>&1; then
|
||||
log "OpenTelemetry auto-instrumentation requested but bash is unavailable; skipping."
|
||||
return
|
||||
fi
|
||||
|
||||
tmp_wrapper="/tmp/elsa-otel-wrapper.sh"
|
||||
cat <<'WRAPPER' > "$tmp_wrapper"
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
. "${OTEL_DOTNET_AUTO_HOME%/}/instrument.sh"
|
||||
exec "$@"
|
||||
WRAPPER
|
||||
chmod +x "$tmp_wrapper"
|
||||
exec "$tmp_wrapper" "$@"
|
||||
}
|
||||
|
||||
if [ -n "${EXTRA_CA_CERT:-}" ]; then
|
||||
install_extra_certificates "$EXTRA_CA_CERT"
|
||||
fi
|
||||
|
||||
maybe_instrument_with_otel "$@"
|
||||
|
||||
exec "$@"
|
||||
95
scripts/test-ca-trust.sh
Executable file
95
scripts/test-ca-trust.sh
Executable file
|
|
@ -0,0 +1,95 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: $0 <image> <tls_app_dir> [docker-run-arg ...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGE="$1"
|
||||
TLS_APP_DIR="$2"
|
||||
shift 2 || true
|
||||
DOCKER_ARGS=("$@")
|
||||
|
||||
SMOKE_DLL=/tls/TlsSmoke.dll
|
||||
PUBLIC_URL="https://example.com"
|
||||
LOCAL_PORT=9443
|
||||
|
||||
run_smoke() {
|
||||
local url="$1"
|
||||
shift
|
||||
docker run --rm \
|
||||
-v "${TLS_APP_DIR}:/tls:ro" \
|
||||
"${DOCKER_ARGS[@]}" \
|
||||
"$@" \
|
||||
"$IMAGE" \
|
||||
dotnet "$SMOKE_DLL" "$url"
|
||||
}
|
||||
|
||||
run_public_test() {
|
||||
echo "[CA] Validating public trust store against ${PUBLIC_URL}" >&2
|
||||
run_smoke "$PUBLIC_URL"
|
||||
}
|
||||
|
||||
start_local_ca_server() {
|
||||
CERT_WORKDIR=$(mktemp -d)
|
||||
|
||||
openssl req -x509 -newkey rsa:2048 -days 2 -nodes -keyout "$CERT_WORKDIR/ca.key" -out "$CERT_WORKDIR/ca.crt" -subj "/CN=ElsaTestCA" >/dev/null 2>&1
|
||||
openssl req -newkey rsa:2048 -nodes -keyout "$CERT_WORKDIR/server.key" -out "$CERT_WORKDIR/server.csr" -subj "/CN=host.docker.internal" >/dev/null 2>&1
|
||||
|
||||
cat <<CERTEXT > "$CERT_WORKDIR/server.ext"
|
||||
subjectAltName = DNS:localhost,DNS:host.docker.internal,IP:127.0.0.1
|
||||
extendedKeyUsage = serverAuth
|
||||
keyUsage = digitalSignature, keyEncipherment
|
||||
CERTEXT
|
||||
|
||||
openssl x509 -req -in "$CERT_WORKDIR/server.csr" -CA "$CERT_WORKDIR/ca.crt" -CAkey "$CERT_WORKDIR/ca.key" -CAcreateserial -out "$CERT_WORKDIR/server.crt" -days 2 -sha256 -extfile "$CERT_WORKDIR/server.ext" >/dev/null 2>&1
|
||||
|
||||
openssl s_server -quiet -accept "$LOCAL_PORT" -www -cert "$CERT_WORKDIR/server.crt" -key "$CERT_WORKDIR/server.key" >/dev/null 2>&1 &
|
||||
SERVER_PID=$!
|
||||
|
||||
for _ in {1..20}; do
|
||||
if nc -z localhost "$LOCAL_PORT" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
echo "$CERT_WORKDIR"
|
||||
}
|
||||
|
||||
run_extra_ca_test() {
|
||||
local cert_dir
|
||||
cert_dir=$(start_local_ca_server)
|
||||
trap "kill ${SERVER_PID:-0} >/dev/null 2>&1 || true; rm -rf '$cert_dir'" EXIT
|
||||
|
||||
echo "[CA] Validating EXTRA_CA_CERT flow against local CA" >&2
|
||||
run_smoke "https://host.docker.internal:${LOCAL_PORT}" \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
-v "${cert_dir}:/certs:ro" \
|
||||
-e EXTRA_CA_CERT=/certs/ca.crt
|
||||
|
||||
echo "[CA] Validating SSL_CERT_FILE fallback" >&2
|
||||
run_smoke "https://host.docker.internal:${LOCAL_PORT}" \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
-v "${cert_dir}:/certs:ro" \
|
||||
-e SSL_CERT_FILE=/certs/ca.crt
|
||||
|
||||
mkdir -p "$cert_dir/dir"
|
||||
cp "$cert_dir/ca.crt" "$cert_dir/dir/custom-ca.crt"
|
||||
openssl rehash "$cert_dir/dir" >/dev/null 2>&1
|
||||
|
||||
echo "[CA] Validating SSL_CERT_DIR fallback" >&2
|
||||
run_smoke "https://host.docker.internal:${LOCAL_PORT}" \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
-v "${cert_dir}:/certs:ro" \
|
||||
-e SSL_CERT_DIR=/certs/dir
|
||||
|
||||
kill "${SERVER_PID:-0}" >/dev/null 2>&1 || true
|
||||
wait "${SERVER_PID:-0}" 2>/dev/null || true
|
||||
rm -rf "$cert_dir"
|
||||
trap - EXIT
|
||||
}
|
||||
|
||||
run_public_test
|
||||
run_extra_ca_test
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=eventargs/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Testing.Shared;
|
||||
|
||||
public class ActivityExecutedEventArgs(ActivityExecutionContext activityExecutionContext) : EventArgs
|
||||
{
|
||||
public ActivityExecutionContext ActivityExecutionContext { get; } = activityExecutionContext;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Runtime.Entities;
|
||||
|
||||
namespace Elsa.Testing.Shared;
|
||||
|
||||
public class ActivityExecutedLogUpdatedEventArgs(WorkflowExecutionContext workflowExecutionContext, ICollection<ActivityExecutionRecord> records) : EventArgs
|
||||
{
|
||||
public WorkflowExecutionContext WorkflowExecutionContext { get; } = workflowExecutionContext;
|
||||
public ICollection<ActivityExecutionRecord> Records { get; } = records;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using Elsa.Workflows.State;
|
||||
|
||||
namespace Elsa.Testing.Shared;
|
||||
|
||||
public class WorkflowStateCommittedEventArgs(WorkflowExecutionContext workflowExecutionContext, WorkflowState workflowState, WorkflowInstance workflowInstance) : EventArgs
|
||||
{
|
||||
public WorkflowExecutionContext WorkflowExecutionContext { get; } = workflowExecutionContext;
|
||||
public WorkflowState WorkflowState { get; } = workflowState;
|
||||
public WorkflowInstance WorkflowInstance { get; } = workflowInstance;
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ using Elsa.Mediator.Contracts;
|
|||
using Elsa.Testing.Shared.Services;
|
||||
using Elsa.Workflows.Management.Notifications;
|
||||
using Elsa.Workflows.Notifications;
|
||||
using Elsa.Workflows.Runtime.Notifications;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Testing.Shared.Handlers;
|
||||
|
|
@ -9,17 +10,38 @@ namespace Elsa.Testing.Shared.Handlers;
|
|||
[UsedImplicitly]
|
||||
public class WorkflowEventHandlers(WorkflowEvents workflowEvents) :
|
||||
INotificationHandler<WorkflowFinished>,
|
||||
INotificationHandler<WorkflowInstanceSaved>
|
||||
INotificationHandler<WorkflowInstanceSaved>,
|
||||
INotificationHandler<WorkflowStateCommitted>,
|
||||
INotificationHandler<ActivityExecuted>,
|
||||
INotificationHandler<ActivityExecutionLogUpdated>
|
||||
{
|
||||
public Task HandleAsync(WorkflowFinished notification, CancellationToken cancellationToken)
|
||||
{
|
||||
workflowEvents.OnWorkflowFinished(new WorkflowFinishedEventArgs(notification.Workflow, notification.WorkflowState));
|
||||
workflowEvents.OnWorkflowFinished(new(notification.Workflow, notification.WorkflowState));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task HandleAsync(WorkflowInstanceSaved notification, CancellationToken cancellationToken)
|
||||
{
|
||||
workflowEvents.OnWorkflowInstanceSaved(new WorkflowInstanceSavedEventArgs(notification.WorkflowInstance));
|
||||
workflowEvents.OnWorkflowInstanceSaved(new(notification.WorkflowInstance));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task HandleAsync(WorkflowStateCommitted notification, CancellationToken cancellationToken)
|
||||
{
|
||||
workflowEvents.OnWorkflowStateCommitted(new(notification.WorkflowExecutionContext, notification.WorkflowState, notification.WorkflowInstance));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task HandleAsync(ActivityExecuted notification, CancellationToken cancellationToken)
|
||||
{
|
||||
workflowEvents.OnActivityExecuted(new(notification.ActivityExecutionContext));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task HandleAsync(ActivityExecutionLogUpdated notification, CancellationToken cancellationToken)
|
||||
{
|
||||
workflowEvents.OnActivityExecutedLogUpdated(new(notification.WorkflowExecutionContext, notification.Records));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,12 @@ public class WorkflowEvents
|
|||
{
|
||||
public event EventHandler<WorkflowFinishedEventArgs>? WorkflowFinished;
|
||||
public event EventHandler<WorkflowInstanceSavedEventArgs>? WorkflowInstanceSaved;
|
||||
public event EventHandler<WorkflowStateCommittedEventArgs>? WorkflowStateCommitted;
|
||||
public event EventHandler<ActivityExecutedEventArgs>? ActivityExecuted;
|
||||
public event EventHandler<ActivityExecutedLogUpdatedEventArgs>? ActivityExecutedLogUpdated;
|
||||
public void OnWorkflowFinished(WorkflowFinishedEventArgs args) => WorkflowFinished?.Invoke(this, args);
|
||||
public void OnWorkflowInstanceSaved(WorkflowInstanceSavedEventArgs args) => WorkflowInstanceSaved?.Invoke(this, args);
|
||||
public void OnWorkflowStateCommitted(WorkflowStateCommittedEventArgs args) => WorkflowStateCommitted?.Invoke(this, args);
|
||||
public void OnActivityExecuted(ActivityExecutedEventArgs args) => ActivityExecuted?.Invoke(this, args);
|
||||
public void OnActivityExecutedLogUpdated(ActivityExecutedLogUpdatedEventArgs args) => ActivityExecutedLogUpdated?.Invoke(this, args);
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using Elsa.Workflows;
|
|||
using Elsa.Workflows.Notifications;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Contracts;
|
||||
using Elsa.Workflows.Runtime.Notifications;
|
||||
using Elsa.Workflows.Runtime.Requests;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
|
@ -13,7 +14,7 @@ namespace Elsa.Testing.Shared;
|
|||
|
||||
public static class DispatchWorkflowExtensions
|
||||
{
|
||||
public static async Task<WorkflowFinished?> DispatchWorkflowAndRunToCompletion(
|
||||
public static async Task<WorkflowStateCommitted?> DispatchWorkflowAndRunToCompletion(
|
||||
this IWorkflow workflowDefinition,
|
||||
Action<IServiceCollection>? configureServices = null,
|
||||
Action<IModule>? configureElsa = null,
|
||||
|
|
@ -21,7 +22,7 @@ public static class DispatchWorkflowExtensions
|
|||
TimeSpan? timeout = null)
|
||||
{
|
||||
var semaphore = new SemaphoreSlim(0, 1);
|
||||
WorkflowFinished? workflowFinishedRecord = null;
|
||||
WorkflowStateCommitted? workflowFinishedRecord = null;
|
||||
|
||||
var host = Host.CreateDefaultBuilder()
|
||||
.ConfigureServices(services =>
|
||||
|
|
@ -29,8 +30,11 @@ public static class DispatchWorkflowExtensions
|
|||
configureServices?.Invoke(services);
|
||||
|
||||
// This notification handler will capture the WorkflowFinished record (to be returned) and release the semaphore.
|
||||
services.AddNotificationHandler<WorkflowFinishedAction, WorkflowFinished>(sp => new(notification =>
|
||||
services.AddNotificationHandler<WorkflowFinishedAction, WorkflowStateCommitted>(sp => new(notification =>
|
||||
{
|
||||
if (notification.WorkflowExecutionContext.Status != WorkflowStatus.Finished)
|
||||
return;
|
||||
|
||||
workflowFinishedRecord = notification;
|
||||
semaphore.Release();
|
||||
}));
|
||||
|
|
@ -75,9 +79,9 @@ public static class DispatchWorkflowExtensions
|
|||
}
|
||||
}
|
||||
|
||||
class WorkflowFinishedAction(Action<WorkflowFinished> action) : INotificationHandler<WorkflowFinished>
|
||||
class WorkflowFinishedAction(Action<WorkflowStateCommitted> action) : INotificationHandler<WorkflowStateCommitted>
|
||||
{
|
||||
public Task HandleAsync(WorkflowFinished notification, CancellationToken cancellationToken)
|
||||
public Task HandleAsync(WorkflowStateCommitted notification, CancellationToken cancellationToken)
|
||||
{
|
||||
action(notification);
|
||||
return Task.CompletedTask;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public class LoggingMiddleware : IActivityExecutionMiddleware
|
|||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
_stopwatch = new Stopwatch();
|
||||
_stopwatch = new();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using Elsa.Workflows.State;
|
||||
|
||||
namespace Elsa.Workflows.Runtime.Notifications;
|
||||
|
||||
public record WorkflowStateCommitted(WorkflowExecutionContext WorkflowExecutionContext, WorkflowState WorkflowState, WorkflowInstance WorkflowInstance) : INotification;
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Workflows.CommitStates;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Runtime.Entities;
|
||||
using Elsa.Workflows.Runtime.Notifications;
|
||||
using Elsa.Workflows.Runtime.Requests;
|
||||
using Elsa.Workflows.State;
|
||||
|
||||
|
|
@ -10,6 +12,7 @@ public class DefaultCommitStateHandler(
|
|||
IWorkflowInstanceManager workflowInstanceManager,
|
||||
IBookmarksPersister bookmarkPersister,
|
||||
IVariablePersistenceManager variablePersistenceManager,
|
||||
IMediator mediator,
|
||||
ILogRecordSink<ActivityExecutionRecord> activityExecutionLogRecordSink,
|
||||
ILogRecordSink<WorkflowExecutionLogRecord> workflowExecutionLogRecordSink) : ICommitStateHandler
|
||||
{
|
||||
|
|
@ -26,9 +29,10 @@ public class DefaultCommitStateHandler(
|
|||
await activityExecutionLogRecordSink.PersistExecutionLogsAsync(workflowExecutionContext, cancellationToken);
|
||||
await workflowExecutionLogRecordSink.PersistExecutionLogsAsync(workflowExecutionContext, cancellationToken);
|
||||
await variablePersistenceManager.SaveVariablesAsync(workflowExecutionContext);
|
||||
await workflowInstanceManager.SaveAsync(workflowState, cancellationToken);
|
||||
var workflowInstance = await workflowInstanceManager.SaveAsync(workflowState, cancellationToken);
|
||||
workflowExecutionContext.ExecutionLog.Clear();
|
||||
workflowExecutionContext.ClearCompletedActivityExecutionContexts();
|
||||
await workflowExecutionContext.ExecuteDeferredTasksAsync();
|
||||
await mediator.SendAsync(new WorkflowStateCommitted(workflowExecutionContext, workflowState, workflowInstance), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ using Elsa.Identity.Providers;
|
|||
using Elsa.Testing.Shared.Services;
|
||||
using Elsa.Workflows.ComponentTests.Decorators;
|
||||
using Elsa.Workflows.ComponentTests.Materializers;
|
||||
using Elsa.Workflows.ComponentTests.Scenarios.Activities.ForEach;
|
||||
using Elsa.Workflows.ComponentTests.Services;
|
||||
using Elsa.Workflows.ComponentTests.WorkflowProviders;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Runtime.Distributed.Extensions;
|
||||
|
|
@ -87,12 +89,14 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl
|
|||
{
|
||||
services
|
||||
.AddSingleton<SignalManager>()
|
||||
.AddScoped<WorkflowEvents>()
|
||||
.AddScoped<AsyncWorkflowRunner>()
|
||||
.AddSingleton<WorkflowEvents>()
|
||||
.AddScoped<WorkflowDefinitionEvents>()
|
||||
.AddSingleton<TriggerChangeTokenSignalEvents>()
|
||||
.AddScoped<IWorkflowMaterializer, TestWorkflowMaterializer>()
|
||||
.AddNotificationHandlersFrom<WorkflowServer>()
|
||||
.AddWorkflowDefinitionProvider<TestWorkflowProvider>()
|
||||
.AddWorkflowsProvider<TestWorkflowProvider>()
|
||||
.AddNotificationHandlersFrom<WorkflowEventHandlers>()
|
||||
.Decorate<IChangeTokenSignaler, EventPublishingChangeTokenSignaler>()
|
||||
;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
using Elsa.Workflows.Runtime.Entities;
|
||||
|
||||
namespace Elsa.Workflows.ComponentTests.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a test workflow execution, including the workflow execution context and activity execution records.
|
||||
/// </summary>
|
||||
/// <param name="WorkflowExecutionContext">The workflow execution context after completion.</param>
|
||||
/// <param name="ActivityExecutionRecords">The collection of activity execution records for the workflow.</param>
|
||||
public record TestWorkflowExecutionResult(WorkflowExecutionContext WorkflowExecutionContext, ICollection<ActivityExecutionRecord> ActivityExecutionRecords);
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Testing.Shared.Services;
|
||||
using Elsa.Workflows.ComponentTests.Models;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Entities;
|
||||
using Elsa.Workflows.Runtime.Messages;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Elsa.Workflows.ComponentTests.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to execute workflows asynchronously and await their completion for testing purposes.
|
||||
/// Tracks activity execution records and workflow completion signals.
|
||||
/// </summary>
|
||||
public class AsyncWorkflowRunner : IDisposable
|
||||
{
|
||||
private readonly IWorkflowRuntime _workflowRuntime;
|
||||
private readonly IIdentityGenerator _identityGenerator;
|
||||
private readonly SignalManager _signalManager;
|
||||
private readonly WorkflowEvents _workflowEvents;
|
||||
private readonly ConcurrentDictionary<string, ActivityExecutionRecord> _activityExecutionRecords = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncWorkflowRunner"/> class.
|
||||
/// </summary>
|
||||
public AsyncWorkflowRunner(IWorkflowRuntime workflowRuntime, IIdentityGenerator identityGenerator, SignalManager signalManager, WorkflowEvents workflowEvents)
|
||||
{
|
||||
_workflowRuntime = workflowRuntime;
|
||||
_identityGenerator = identityGenerator;
|
||||
_signalManager = signalManager;
|
||||
_workflowEvents = workflowEvents;
|
||||
|
||||
_workflowEvents.WorkflowStateCommitted += OnWorkflowStateCommitted;
|
||||
_workflowEvents.ActivityExecutedLogUpdated += OnActivityExecutedLogUpdated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified workflow definition asynchronously and waits for its completion.
|
||||
/// Returns the workflow execution context and activity execution records.
|
||||
/// </summary>
|
||||
/// <param name="workflowDefinitionHandle">The handle of the workflow definition to execute.</param>
|
||||
/// <returns>A <see cref="TestWorkflowExecutionResult"/> containing the workflow execution context and activity execution records.</returns>
|
||||
public async Task<TestWorkflowExecutionResult> RunAndAwaitWorkflowCompletionAsync(WorkflowDefinitionHandle workflowDefinitionHandle)
|
||||
{
|
||||
var workflowInstanceId = _identityGenerator.GenerateId();
|
||||
var workflowClient = await _workflowRuntime.CreateClientAsync(workflowInstanceId);
|
||||
await workflowClient.CreateInstanceAsync(new()
|
||||
{
|
||||
WorkflowDefinitionHandle = workflowDefinitionHandle
|
||||
});
|
||||
_activityExecutionRecords.Clear();
|
||||
await workflowClient.RunInstanceAsync(RunWorkflowInstanceRequest.Empty);
|
||||
var signalName = GetSignalName(workflowInstanceId);
|
||||
var workflowExecutionContext = await _signalManager.WaitAsync<WorkflowExecutionContext>(signalName);
|
||||
return new(workflowExecutionContext, _activityExecutionRecords.Values.ToList());
|
||||
}
|
||||
|
||||
private void OnWorkflowStateCommitted(object? sender, WorkflowStateCommittedEventArgs e)
|
||||
{
|
||||
if (e.WorkflowExecutionContext.Status != WorkflowStatus.Finished)
|
||||
return;
|
||||
|
||||
var signalName = GetSignalName(e.WorkflowExecutionContext.Id);
|
||||
_signalManager.Trigger(signalName, e.WorkflowExecutionContext);
|
||||
}
|
||||
|
||||
private void OnActivityExecutedLogUpdated(object? sender, ActivityExecutedLogUpdatedEventArgs e)
|
||||
{
|
||||
foreach (var record in e.Records)
|
||||
_activityExecutionRecords[record.Id] = record;
|
||||
}
|
||||
|
||||
private static string GetSignalName(string workflowInstanceId) => $"WorkflowInstanceCompleted-{workflowInstanceId}";
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from workflow events and releases resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_workflowEvents.WorkflowStateCommitted -= OnWorkflowStateCommitted;
|
||||
_workflowEvents.ActivityExecutedLogUpdated -= OnActivityExecutedLogUpdated;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using Elsa.Common.Models;
|
||||
using Elsa.Workflows.ComponentTests.Abstractions;
|
||||
using Elsa.Workflows.ComponentTests.Fixtures;
|
||||
using Elsa.Workflows.ComponentTests.Scenarios.Activities.ForEach.Workflows;
|
||||
using Elsa.Workflows.ComponentTests.Services;
|
||||
using Elsa.Workflows.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.ForEach;
|
||||
|
||||
public class ForEachWorkflowTests : AppComponentTest
|
||||
{
|
||||
private readonly AsyncWorkflowRunner _workflowRunner;
|
||||
|
||||
public ForEachWorkflowTests(App app) : base(app)
|
||||
{
|
||||
_workflowRunner = Scope.ServiceProvider.GetRequiredService<AsyncWorkflowRunner>();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ForEach activity executes child activity for each collection item and supports blocking activities")]
|
||||
public async Task ForEachActivity_ExecutesChildActivity_ForEachCollectionItem_AndSupportsBlocking()
|
||||
{
|
||||
var result = await _workflowRunner.RunAndAwaitWorkflowCompletionAsync(WorkflowDefinitionHandle.ByDefinitionId(ForEachWorkflow.DefinitionId, VersionOptions.Published));
|
||||
var writeLineExecutionRecords = result.ActivityExecutionRecords.Where(x => x.ActivityId == "WriteLine1").ToList();
|
||||
|
||||
// Assert that the workflow executed the expected number of activities.
|
||||
Assert.Equal(3, writeLineExecutionRecords.Count);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Scheduling.Activities;
|
||||
using Elsa.Workflows.Activities;
|
||||
|
||||
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.ForEach.Workflows;
|
||||
|
||||
public class ForEachWorkflow : WorkflowBase
|
||||
{
|
||||
public static readonly string DefinitionId = Guid.NewGuid().ToString();
|
||||
protected override void Build(IWorkflowBuilder builder)
|
||||
{
|
||||
builder.WithDefinitionId(DefinitionId);
|
||||
builder.Root = new Sequence
|
||||
{
|
||||
Activities =
|
||||
{
|
||||
new ForEach<string>(["a", "b", "c"])
|
||||
{
|
||||
Body = new Sequence
|
||||
{
|
||||
Activities =
|
||||
{
|
||||
new WriteLine(context => $"Processing item: {context.GetVariable<string>("CurrentValue")}"),
|
||||
Delay.FromMilliseconds(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -26,11 +26,11 @@ public class DispatchWorkflowsTests : AppComponentTest
|
|||
public async Task DispatchAndWaitWorkflow_ShouldWaitForChildWorkflowToComplete()
|
||||
{
|
||||
var workflowClient = await _workflowRuntime.CreateClientAsync();
|
||||
await workflowClient.CreateInstanceAsync(new CreateWorkflowInstanceRequest
|
||||
await workflowClient.CreateInstanceAsync(new()
|
||||
{
|
||||
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(DispatchAndWaitWorkflow.DefinitionId, VersionOptions.Published)
|
||||
});
|
||||
await workflowClient.RunInstanceAsync(RunWorkflowInstanceRequest.Empty);
|
||||
await _signalManager.WaitAsync<string>("Completed");
|
||||
await _signalManager.WaitAsync("Completed");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Activities.IntegrationTests;
|
||||
|
||||
public class ForEachTests
|
||||
{
|
||||
private readonly CapturingTextWriter _capturingTextWriter = new();
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public ForEachTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_serviceProvider = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ForEach executes each activity for every item in the collection")]
|
||||
public async Task ForEach_ExecutesEachActivity_ForEveryItem()
|
||||
{
|
||||
var expectedLines = new[] {"a", "b", "c"};
|
||||
var forEach = new ForEach<string>(expectedLines)
|
||||
{
|
||||
Body = new WriteLine(context => context.GetVariable<string>("CurrentValue"))
|
||||
};
|
||||
await _serviceProvider.RunActivityAsync(forEach);
|
||||
Assert.Equal(expectedLines, _capturingTextWriter.Lines);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue