feat: introduce HTTP webhooks module
Adds a new `Elsa.Http.Webhooks` module, enabling workflows to receive incoming webhook events and dispatch outgoing webhooks. This integrates the WebhooksCore library. Further improvements include: - Enhanced validation for configured application instance names, providing clearer feedback, especially regarding Azure Service Bus entity name limits. - Improved API error reporting for shell reload operations, distinguishing between blueprint not found (404) and other failures (503). - Updated release announcement rendering to dynamically reference the correct major.minor release line for feedback messages.
This commit is contained in:
commit
103028452f
|
|
@ -194,7 +194,7 @@ def render_discord(
|
|||
intro = discord_intro(product, version, release_kind)
|
||||
highlight_text = render_discord_highlights(highlights)
|
||||
upgrade_notes = render_upgrade_notes(release_kind)
|
||||
validation = render_validation_note(release_kind)
|
||||
validation = render_validation_note(release_kind, version)
|
||||
|
||||
return f"""\
|
||||
:rocket: **{heading}**
|
||||
|
|
@ -264,10 +264,11 @@ def render_upgrade_notes(release_kind: str) -> str:
|
|||
return f"{heading}\n{body}"
|
||||
|
||||
|
||||
def render_validation_note(release_kind: str) -> str:
|
||||
def render_validation_note(release_kind: str, version: str) -> str:
|
||||
if release_kind == "stable":
|
||||
return """### :raised_hands: Feedback welcome
|
||||
Please report issues, regressions, or upgrade notes you run into so we can keep improving the 3.7 line."""
|
||||
release_line = ".".join(version.split(".")[:2]) if "." in version else version
|
||||
return f"""### :raised_hands: Feedback welcome
|
||||
Please report issues, regressions, or upgrade notes you run into so we can keep improving the {release_line} line."""
|
||||
|
||||
return """### :test_tube: Please test it
|
||||
This release is intended for testing and validation before the final stable release. Feedback, bug reports, and PRs are very welcome :raised_hands:"""
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@
|
|||
<PackageVersion Include="Testcontainers.RabbitMq" Version="4.9.0"/>
|
||||
<PackageVersion Include="Testcontainers.Redis" Version="4.9.0"/>
|
||||
<PackageVersion Include="ThrottleDebounce" Version="2.0.2"/>
|
||||
<PackageVersion Include="WebhooksCore" Version="0.0.6"/>
|
||||
<PackageVersion Include="WebhooksCore" Version="0.0.1"/>
|
||||
<PackageVersion Include="xunit" Version="2.9.3"/>
|
||||
<PackageVersion Include="xunit.abstractions" Version="2.0.3"/>
|
||||
<PackageVersion Include="xunit.assert" Version="2.9.3"/>
|
||||
|
|
|
|||
15
Elsa.sln
15
Elsa.sln
|
|
@ -199,6 +199,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "http", "http", "{C55015F0-E
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Http", "src\modules\Elsa.Http\Elsa.Http.csproj", "{E0B22AB7-7CB5-6D17-562C-4A989DC61F8A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Http.Webhooks", "src\modules\Elsa.Http.Webhooks\Elsa.Http.Webhooks.csproj", "{ABC40471-5A32-0AC0-594B-FF19B66E2493}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Expressions.CSharp", "src\modules\Elsa.Expressions.CSharp\Elsa.Expressions.CSharp.csproj", "{16C3FBDE-A832-E3CD-5FBF-F51744D1F79B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Expressions.JavaScript", "src\modules\Elsa.Expressions.JavaScript\Elsa.Expressions.JavaScript.csproj", "{099D5DCE-CCF7-16FE-6EDC-A64B694F50BE}"
|
||||
|
|
@ -859,6 +861,18 @@ Global
|
|||
{E0B22AB7-7CB5-6D17-562C-4A989DC61F8A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E0B22AB7-7CB5-6D17-562C-4A989DC61F8A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E0B22AB7-7CB5-6D17-562C-4A989DC61F8A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{ABC40471-5A32-0AC0-594B-FF19B66E2493}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{ABC40471-5A32-0AC0-594B-FF19B66E2493}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{ABC40471-5A32-0AC0-594B-FF19B66E2493}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{ABC40471-5A32-0AC0-594B-FF19B66E2493}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{ABC40471-5A32-0AC0-594B-FF19B66E2493}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{ABC40471-5A32-0AC0-594B-FF19B66E2493}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{ABC40471-5A32-0AC0-594B-FF19B66E2493}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{ABC40471-5A32-0AC0-594B-FF19B66E2493}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{ABC40471-5A32-0AC0-594B-FF19B66E2493}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{ABC40471-5A32-0AC0-594B-FF19B66E2493}.Release|x64.Build.0 = Release|Any CPU
|
||||
{ABC40471-5A32-0AC0-594B-FF19B66E2493}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{ABC40471-5A32-0AC0-594B-FF19B66E2493}.Release|x86.Build.0 = Release|Any CPU
|
||||
{16C3FBDE-A832-E3CD-5FBF-F51744D1F79B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{16C3FBDE-A832-E3CD-5FBF-F51744D1F79B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{16C3FBDE-A832-E3CD-5FBF-F51744D1F79B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
|
|
@ -2214,6 +2228,7 @@ Global
|
|||
{6F14B066-DF7B-2409-59D8-CCCA90A4974C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{C55015F0-E9DF-4BF9-8131-D7539E938220} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
||||
{E0B22AB7-7CB5-6D17-562C-4A989DC61F8A} = {C55015F0-E9DF-4BF9-8131-D7539E938220}
|
||||
{ABC40471-5A32-0AC0-594B-FF19B66E2493} = {C55015F0-E9DF-4BF9-8131-D7539E938220}
|
||||
{16C3FBDE-A832-E3CD-5FBF-F51744D1F79B} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76}
|
||||
{099D5DCE-CCF7-16FE-6EDC-A64B694F50BE} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76}
|
||||
{08B69CC4-B5F0-44E8-FA7A-6BF6F00CA40A} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76}
|
||||
|
|
|
|||
34
announcements/discord-3.6.3.md
Normal file
34
announcements/discord-3.6.3.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
:rocket: **Elsa Workflows 3.6.3 is here!**
|
||||
|
||||
We've published the stable **Elsa 3.6.3** patch release across **Elsa Core**, **Elsa Studio**, and **Elsa Extensions**.
|
||||
|
||||
:point_right: Core: <https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3>
|
||||
:point_right: Studio: <https://github.com/elsa-workflows/elsa-studio/releases/tag/3.6.3>
|
||||
:point_right: Extensions: <https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.6.3>
|
||||
|
||||
Packages are available on NuGet.
|
||||
|
||||
### :sparkles: Highlights
|
||||
|
||||
:tools: **Azure Service Bus startup stability**
|
||||
Core now supports stable application instance names for clustered hosts, plus deterministic shortening for long names.
|
||||
|
||||
:clock3: **Blank Cron expressions behave as disabled again**
|
||||
Blank Cron values no longer block publishing or throw during workflow execution. Invalid nonblank Cron expressions still fail validation.
|
||||
|
||||
:memo: **Better workflow publish feedback**
|
||||
Publish APIs now surface validation warnings and errors instead of reporting silent success.
|
||||
|
||||
:gear: **Quartz and MassTransit extension fixes**
|
||||
Extensions now ensure durable Quartz jobs before scheduling triggers and use shorter MassTransit endpoint names.
|
||||
|
||||
:jigsaw: **Studio package alignment**
|
||||
Studio now consumes `Elsa.Api.Client` 3.6.3.
|
||||
|
||||
### :tools: Upgrade notes
|
||||
|
||||
No intentional breaking changes. If you run clustered Elsa hosts on Azure Service Bus, review the stable application instance name option.
|
||||
|
||||
### :raised_hands: Feedback welcome
|
||||
|
||||
Please report upgrade issues or regressions so we can keep the 3.6 line solid.
|
||||
29
announcements/discord-3.7.1.md
Normal file
29
announcements/discord-3.7.1.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
:rocket: **Elsa Workflows 3.7.1 is here!**
|
||||
|
||||
We've published the stable **Elsa 3.7.1** patch release across **Elsa Core**, **Elsa Studio**, and **Elsa Extensions**. Packages are available on NuGet.
|
||||
|
||||
:point_right: Core: <https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.1>
|
||||
:point_right: Studio: <https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.1>
|
||||
:point_right: Extensions: <https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.7.1>
|
||||
|
||||
### :sparkles: Highlights
|
||||
|
||||
:cloud: **Azure Service Bus hosting reliability**
|
||||
Elsa Core now supports stable application instance names so clustered hosts can reuse Azure Service Bus-backed change-token subscriptions and queues across restarts.
|
||||
|
||||
:straight_ruler: **Safer Azure Service Bus entity names**
|
||||
Long configured instance names are shortened deterministically, and Extensions shortens MassTransit endpoint suffixes to reduce Azure Service Bus entity-name pressure.
|
||||
|
||||
:clock3: **Quartz scheduling fix**
|
||||
Extensions now ensures the durable Quartz workflow job exists before scheduling triggers, preventing trigger storage failures.
|
||||
|
||||
:link: **Package alignment**
|
||||
Studio now consumes `Elsa.Api.Client` 3.7.1, and Extensions aligns with Core and Studio 3.7.1 packages.
|
||||
|
||||
### :tools: Upgrade notes
|
||||
|
||||
No breaking changes are expected. If you run clustered Elsa hosts with Azure Service Bus, configure a stable per-instance name for each concurrently running host. Quartz-backed scheduling deployments should pick up the durable-job fix automatically.
|
||||
|
||||
### :raised_hands: Feedback welcome
|
||||
|
||||
Please report upgrade issues, regressions, or deployment notes you run into so we can keep improving the 3.7 line.
|
||||
118
announcements/elsa-3.6.3-announcement-pack.md
Normal file
118
announcements/elsa-3.6.3-announcement-pack.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# Elsa 3.6.3 Announcement Pack
|
||||
|
||||
## Facts
|
||||
|
||||
- Product: Elsa Workflows across Elsa Core, Elsa Studio, and Elsa Extensions.
|
||||
- Version: 3.6.3.
|
||||
- Release kind: stable patch release.
|
||||
- Publishing mode: draft-only until approved.
|
||||
- GitHub releases:
|
||||
- Elsa Core: https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3
|
||||
- Elsa Studio: https://github.com/elsa-workflows/elsa-studio/releases/tag/3.6.3
|
||||
- Elsa Extensions: https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.6.3
|
||||
- Package availability verified on NuGet flat-container metadata for representative packages:
|
||||
- Elsa 3.6.3.
|
||||
- Elsa.Api.Client 3.6.3.
|
||||
- Elsa.Studio.Core 3.6.3.
|
||||
- Elsa.Scheduling.Quartz 3.6.3.
|
||||
- No intentional breaking changes are called out in the three release notes.
|
||||
|
||||
## Discord
|
||||
|
||||
:rocket: **Elsa Workflows 3.6.3 is here!**
|
||||
|
||||
We've published the stable **Elsa 3.6.3** patch release across **Elsa Core**, **Elsa Studio**, and **Elsa Extensions**.
|
||||
|
||||
:point_right: Core: <https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3>
|
||||
:point_right: Studio: <https://github.com/elsa-workflows/elsa-studio/releases/tag/3.6.3>
|
||||
:point_right: Extensions: <https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.6.3>
|
||||
|
||||
Packages are available on NuGet.
|
||||
|
||||
### :sparkles: Highlights
|
||||
|
||||
:tools: **Azure Service Bus startup stability**
|
||||
Core now supports an opt-in stable application instance name provider so clustered hosts can reuse Azure Service Bus transport identities across restarts instead of accumulating new subscriptions. Long configured instance names are shortened deterministically, which helps with Kubernetes pod names and Azure Service Bus entity-name limits.
|
||||
|
||||
:clock3: **Blank Cron expressions behave as disabled again**
|
||||
Blank or whitespace Cron values no longer block workflow publishing and no longer throw when a Cron activity runs inside a workflow. Invalid nonblank Cron expressions still fail validation.
|
||||
|
||||
:memo: **Better workflow publish feedback**
|
||||
Publish, bulk-publish, and save-and-publish responses now honor failed publish results and surface validation warnings or errors so clients can show actionable feedback instead of reporting silent success.
|
||||
|
||||
:gear: **Quartz and MassTransit extension fixes**
|
||||
Extensions now ensure durable Quartz jobs exist before scheduling triggers, and MassTransit endpoint suffixes were shortened to reduce Azure Service Bus subscription-name pressure.
|
||||
|
||||
:jigsaw: **Studio package alignment**
|
||||
Studio now consumes `Elsa.Api.Client` 3.6.3, keeping the Studio packages aligned with the Core 3.6.3 patch release.
|
||||
|
||||
### :tools: Upgrade notes
|
||||
|
||||
There are no intentional breaking changes in this patch release. If you run clustered Elsa hosts on Azure Service Bus, review the stable application instance name option and configure a stable unique name per process or pod where appropriate.
|
||||
|
||||
### :raised_hands: Feedback welcome
|
||||
|
||||
Please report upgrade issues, regressions, or notes from production-like environments so we can keep the 3.6 line solid.
|
||||
|
||||
## LinkedIn
|
||||
|
||||
Elsa Workflows 3.6.3 is now available.
|
||||
|
||||
This is a stable patch release across Elsa Core, Elsa Studio, and Elsa Extensions, focused on reliability fixes for production operators and cleaner feedback for workflow publishing.
|
||||
|
||||
The main improvements are around clustered hosting and scheduling:
|
||||
|
||||
- Azure Service Bus hosts can now opt into stable application instance names, helping avoid subscription buildup across restarts.
|
||||
- Long configured instance names are shortened deterministically, which is useful for Kubernetes and transport entity-name limits.
|
||||
- Blank Cron expressions are treated as disabled again, restoring a smoother workflow publishing experience while still rejecting invalid Cron values.
|
||||
- Workflow publish APIs now return clearer validation warnings and errors.
|
||||
- Extensions include Quartz durable trigger scheduling fixes and shorter MassTransit endpoint names.
|
||||
- Studio has been aligned with the Elsa Core 3.6.3 client packages.
|
||||
|
||||
No intentional breaking changes are included.
|
||||
|
||||
Release notes:
|
||||
https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3
|
||||
https://github.com/elsa-workflows/elsa-studio/releases/tag/3.6.3
|
||||
https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.6.3
|
||||
|
||||
## X Single-Post Option
|
||||
|
||||
Elsa Workflows 3.6.3 is available across Core, Studio, and Extensions.
|
||||
|
||||
This stable patch release improves Azure Service Bus clustered hosting, blank Cron handling, publish validation feedback, Quartz durable trigger scheduling, and Studio package alignment.
|
||||
|
||||
Core notes: https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3
|
||||
|
||||
## X Thread Option
|
||||
|
||||
1/ Elsa Workflows 3.6.3 is available across Core, Studio, and Extensions.
|
||||
|
||||
This stable patch release focuses on reliability: Azure Service Bus clustered hosting, Cron publishing behavior, publish validation responses, Quartz scheduling, and package alignment.
|
||||
|
||||
Core notes: https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3
|
||||
|
||||
2/ Highlights:
|
||||
|
||||
- Stable application instance names for clustered Azure Service Bus hosts.
|
||||
- Deterministic shortening for long configured instance names.
|
||||
- Blank Cron expressions behave as disabled again.
|
||||
- Publish APIs surface validation warnings and errors.
|
||||
|
||||
3/ Extensions add Quartz durable trigger scheduling fixes and shorter MassTransit endpoint names.
|
||||
|
||||
Studio is aligned with `Elsa.Api.Client` 3.6.3.
|
||||
|
||||
No intentional breaking changes.
|
||||
|
||||
Studio: https://github.com/elsa-workflows/elsa-studio/releases/tag/3.6.3
|
||||
Extensions: https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.6.3
|
||||
|
||||
## Links
|
||||
|
||||
- Elsa Core release: https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3
|
||||
- Elsa Studio release: https://github.com/elsa-workflows/elsa-studio/releases/tag/3.6.3
|
||||
- Elsa Extensions release: https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.6.3
|
||||
- NuGet package search: https://www.nuget.org/packages?q=Elsa
|
||||
|
||||
<!-- Review and approve exact copy before publishing. -->
|
||||
90
announcements/elsa-3.7.1-announcement-pack.md
Normal file
90
announcements/elsa-3.7.1-announcement-pack.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Elsa 3.7.1 Announcement Pack
|
||||
|
||||
## Facts
|
||||
|
||||
- Product: Elsa Workflows
|
||||
- Version: 3.7.1
|
||||
- Release kind: stable patch release
|
||||
- Published: June 21, 2026
|
||||
- Repositories: Elsa Core, Elsa Studio, Elsa Extensions
|
||||
- GitHub releases are public and not prereleases.
|
||||
- Representative NuGet packages verified at 3.7.1:
|
||||
- Elsa, Elsa.Api.Client, Elsa.Hosting.Management, Elsa.Workflows.Runtime, Elsa.Workflows.Management
|
||||
- Elsa.Studio, Elsa.Studio.Core, Elsa.Studio.Workflows, Elsa.Studio.Workflows.Designer, Elsa.Studio.Authentication.ElsaIdentity
|
||||
- Elsa.Scheduling.Quartz, Elsa.Caching.Distributed.MassTransit, Elsa.ServiceBus.MassTransit.AzureServiceBus, Elsa.ServiceBus.MassTransit, Elsa.ServiceBus.AzureServiceBus
|
||||
|
||||
## Discord
|
||||
|
||||
:rocket: **Elsa Workflows 3.7.1 is here!**
|
||||
|
||||
We've published the stable **Elsa 3.7.1** patch release across **Elsa Core**, **Elsa Studio**, and **Elsa Extensions**. Packages are available on NuGet.
|
||||
|
||||
:point_right: Core: <https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.1>
|
||||
:point_right: Studio: <https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.1>
|
||||
:point_right: Extensions: <https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.7.1>
|
||||
|
||||
### :sparkles: Highlights
|
||||
|
||||
:cloud: **Azure Service Bus hosting reliability**
|
||||
Elsa Core now supports stable application instance names so clustered hosts can reuse Azure Service Bus-backed change-token subscriptions and queues across restarts.
|
||||
|
||||
:straight_ruler: **Safer Azure Service Bus entity names**
|
||||
Long configured instance names are shortened deterministically, and Extensions shortens MassTransit endpoint suffixes to reduce Azure Service Bus entity-name pressure.
|
||||
|
||||
:clock3: **Quartz scheduling fix**
|
||||
Extensions now ensures the durable Quartz workflow job exists before scheduling triggers, preventing trigger storage failures.
|
||||
|
||||
:link: **Package alignment**
|
||||
Studio now consumes `Elsa.Api.Client` 3.7.1, and Extensions aligns with Core and Studio 3.7.1 packages.
|
||||
|
||||
### :tools: Upgrade notes
|
||||
|
||||
No breaking changes are expected. If you run clustered Elsa hosts with Azure Service Bus, configure a stable per-instance name for each concurrently running host. Quartz-backed scheduling deployments should pick up the durable-job fix automatically.
|
||||
|
||||
### :raised_hands: Feedback welcome
|
||||
|
||||
Please report upgrade issues, regressions, or deployment notes you run into so we can keep improving the 3.7 line.
|
||||
|
||||
## LinkedIn
|
||||
|
||||
Elsa Workflows 3.7.1 is now available as a stable patch release across Elsa Core, Elsa Studio, and Elsa Extensions.
|
||||
|
||||
This release focuses on operational reliability for production deployments:
|
||||
|
||||
- Azure Service Bus-backed clustered hosts can now use stable application instance names, reducing restart-driven entity buildup.
|
||||
- Long configured instance names are shortened deterministically so deployment-provided names can remain stable while fitting Azure Service Bus limits.
|
||||
- Quartz-backed scheduling now verifies the durable workflow job before scheduling triggers.
|
||||
- Studio and Extensions packages are aligned with the Elsa 3.7.1 package set.
|
||||
|
||||
No breaking changes are expected. If you run Elsa in a clustered Azure Service Bus setup, review the upgrade notes around `ApplicationInstanceOptions.InstanceName` and `ApplicationInstanceOptions.InstanceNameEnvironmentVariable`.
|
||||
|
||||
Release notes: https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.1
|
||||
|
||||
## X single-post option
|
||||
|
||||
Elsa Workflows 3.7.1 is now available across Core, Studio, and Extensions.
|
||||
|
||||
This stable patch improves Azure Service Bus clustered hosting reliability, reduces entity-name pressure, fixes Quartz durable trigger scheduling, and aligns Studio/Extensions packages with 3.7.1.
|
||||
|
||||
https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.1
|
||||
|
||||
## X thread option
|
||||
|
||||
1/ Elsa Workflows 3.7.1 is now available across Core, Studio, and Extensions.
|
||||
|
||||
This stable patch focuses on production reliability for clustered hosting, Azure Service Bus deployments, Quartz scheduling, and package alignment.
|
||||
|
||||
https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.1
|
||||
|
||||
2/ Core adds stable application instance names via `ApplicationInstanceOptions.InstanceName` and `InstanceNameEnvironmentVariable`, helping Azure Service Bus-backed clustered hosts reuse subscriptions and queues across restarts.
|
||||
|
||||
3/ Extensions reduces Azure Service Bus entity-name pressure by shortening MassTransit endpoint suffixes, and fixes Quartz-backed scheduling by ensuring the durable workflow job exists before triggers are scheduled.
|
||||
|
||||
4/ Studio now consumes the published `Elsa.Api.Client` 3.7.1 package, while Extensions aligns with the Elsa Core and Studio 3.7.1 package set.
|
||||
|
||||
## Links
|
||||
|
||||
- Core: https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.1
|
||||
- Studio: https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.1
|
||||
- Extensions: https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.7.1
|
||||
- NuGet package search: https://www.nuget.org/packages?q=elsa
|
||||
395
docs/codebase/.codebase-scan.txt
Normal file
395
docs/codebase/.codebase-scan.txt
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
|
||||
=== DIRECTORY TREE (max depth 3, source files only) ===
|
||||
.DS_Store
|
||||
.agents
|
||||
.agents/skills
|
||||
.agents/skills/elsa-release-announcements
|
||||
.agents/skills/elsa-release-announcements/SKILL.md
|
||||
.agents/skills/elsa-release-announcements/agents
|
||||
.agents/skills/elsa-release-announcements/scripts
|
||||
.claude
|
||||
.claude/worktrees
|
||||
.config
|
||||
.config/dotnet-tools.json
|
||||
.editorconfig
|
||||
.github
|
||||
.github/ISSUE_TEMPLATE
|
||||
.github/ISSUE_TEMPLATE/bug_report.md
|
||||
.github/ISSUE_TEMPLATE/documentation.md
|
||||
.github/ISSUE_TEMPLATE/feature_request.md
|
||||
.github/ISSUE_TEMPLATE/improvement.md
|
||||
.github/ISSUE_TEMPLATE/performance.md
|
||||
.github/ISSUE_TEMPLATE/task.md
|
||||
.github/agents
|
||||
.github/agents/copilot-instructions.md
|
||||
.github/agents/release-notes.agent.md
|
||||
.github/agents/speckit.analyze.agent.md
|
||||
.github/agents/speckit.checklist.agent.md
|
||||
.github/agents/speckit.clarify.agent.md
|
||||
.github/agents/speckit.constitution.agent.md
|
||||
.github/agents/speckit.implement.agent.md
|
||||
.github/agents/speckit.plan.agent.md
|
||||
.github/agents/speckit.specify.agent.md
|
||||
.github/agents/speckit.tasks.agent.md
|
||||
.github/agents/speckit.taskstoissues.agent.md
|
||||
.github/copilot-instructions.md
|
||||
.github/copilot-release-notes-playbook.md
|
||||
.github/dependabot.yml
|
||||
.github/prompts
|
||||
.github/prompts/speckit.analyze.prompt.md
|
||||
.github/prompts/speckit.checklist.prompt.md
|
||||
.github/prompts/speckit.clarify.prompt.md
|
||||
.github/prompts/speckit.constitution.prompt.md
|
||||
.github/prompts/speckit.implement.prompt.md
|
||||
.github/prompts/speckit.plan.prompt.md
|
||||
.github/prompts/speckit.specify.prompt.md
|
||||
.github/prompts/speckit.tasks.prompt.md
|
||||
.github/prompts/speckit.taskstoissues.prompt.md
|
||||
.github/pull_request_template.md
|
||||
.github/workflows
|
||||
.github/workflows/bounty.yml
|
||||
.github/workflows/claude-code-review.yml
|
||||
.github/workflows/claude.yml
|
||||
.github/workflows/copilot-setup-steps.yml
|
||||
.github/workflows/docker-ca.yml
|
||||
.github/workflows/packages.yml
|
||||
.github/workflows/pr.yml
|
||||
.github/workflows/stale.yml
|
||||
.gitignore
|
||||
.idea
|
||||
.idea/.idea.Elsa
|
||||
.idea/.idea.Elsa/.idea
|
||||
.idea/.idea.Elsa/.idea/.gitignore
|
||||
.idea/.idea.Elsa/.idea/.name
|
||||
.idea/.idea.Elsa/.idea/copilotDiffState.xml
|
||||
.idea/.idea.Elsa/.idea/dataSources
|
||||
.idea/.idea.Elsa/.idea/dataSources.local.xml
|
||||
.idea/.idea.Elsa/.idea/dataSources.xml
|
||||
.idea/.idea.Elsa/.idea/db-forest-config.xml
|
||||
.idea/.idea.Elsa/.idea/encodings.xml
|
||||
.idea/.idea.Elsa/.idea/indexLayout.xml
|
||||
.idea/.idea.Elsa/.idea/projectSettingsUpdater.xml
|
||||
.idea/.idea.Elsa/.idea/vcs.xml
|
||||
.idea/.idea.Elsa/.idea/workspace.xml
|
||||
.nuke
|
||||
.nuke/build.schema.json
|
||||
.nuke/parameters.json
|
||||
.specify
|
||||
.specify/memory
|
||||
.specify/memory/constitution.md
|
||||
.specify/scripts
|
||||
.specify/scripts/bash
|
||||
.specify/scripts/bash/check-prerequisites.sh
|
||||
.specify/scripts/bash/common.sh
|
||||
.specify/scripts/bash/create-new-feature.sh
|
||||
.specify/scripts/bash/setup-plan.sh
|
||||
.specify/scripts/bash/update-agent-context.sh
|
||||
.specify/templates
|
||||
.specify/templates/agent-file-template.md
|
||||
.specify/templates/checklist-template.md
|
||||
.specify/templates/constitution-template.md
|
||||
.specify/templates/plan-template.md
|
||||
.specify/templates/spec-template.md
|
||||
.specify/templates/tasks-template.md
|
||||
.vscode
|
||||
.vscode/settings.json
|
||||
CONTRIBUTING.md
|
||||
Directory.Build.props
|
||||
Directory.Build.targets
|
||||
Directory.Packages.props
|
||||
Elsa.sln
|
||||
Elsa.sln.DotSettings
|
||||
Elsa.sln.DotSettings.user
|
||||
LICENSE
|
||||
NuGet.Config
|
||||
README.md
|
||||
announcements
|
||||
announcements/discord-3.6.3.md
|
||||
announcements/discord-3.7.1.md
|
||||
announcements/elsa-3.6.3-announcement-pack.md
|
||||
announcements/elsa-3.7.1-announcement-pack.md
|
||||
artifacts
|
||||
artifacts/.DS_Store
|
||||
artifacts/package-manifest-check
|
||||
artifacts/package-manifest-check/Elsa.Caching.1.0.0.nupkg
|
||||
artifacts/package-manifest-check/Elsa.Caching.1.0.0.snupkg
|
||||
artifacts/tls-smoke
|
||||
artifacts/tls-smoke/TlsSmoke
|
||||
artifacts/tls-smoke/TlsSmoke.deps.json
|
||||
artifacts/tls-smoke/TlsSmoke.dll
|
||||
artifacts/tls-smoke/TlsSmoke.pdb
|
||||
artifacts/tls-smoke/TlsSmoke.runtimeconfig.json
|
||||
artifacts/tls-smoke/TlsSmoke.xml
|
||||
build.cmd
|
||||
build.ps1
|
||||
build.sh
|
||||
design
|
||||
design/.DS_Store
|
||||
design/artwork
|
||||
design/artwork/android-elsa-portrait.png
|
||||
design/artwork/creative-elsa-animation.gif
|
||||
design/artwork/elsa-3.1-art.png
|
||||
design/artwork/elsa-logo-art.png
|
||||
design/artwork/elsa-logo-art.psd
|
||||
design/artwork/elsa-v3-avatar.png
|
||||
design/artwork/elsa-waves.mp4
|
||||
design/artwork/glass.ai
|
||||
design/custom
|
||||
design/custom/1x
|
||||
design/custom/1x/Artboard 1.png
|
||||
design/custom/2x
|
||||
design/custom/2x/Artboard 1@2x.png
|
||||
design/custom/2x/elsa-logo.png
|
||||
design/custom/icon.ai
|
||||
design/custom/icon.png
|
||||
design/custom/icon.psd
|
||||
design/elsa-workflows-workflow-engine-for-dotnet.png
|
||||
design/github-social-preview-banner-for-elsa.psd
|
||||
design/icon
|
||||
design/icon/dark
|
||||
design/icon/dark/android-icon-144x144.png
|
||||
design/icon/dark/android-icon-192x192.png
|
||||
design/icon/dark/android-icon-36x36.png
|
||||
design/icon/dark/android-icon-48x48.png
|
||||
design/icon/dark/android-icon-72x72.png
|
||||
design/icon/dark/android-icon-96x96.png
|
||||
design/icon/dark/apple-icon-114x114.png
|
||||
design/icon/dark/apple-icon-120x120.png
|
||||
design/icon/dark/apple-icon-144x144.png
|
||||
design/icon/dark/apple-icon-152x152.png
|
||||
design/icon/dark/apple-icon-180x180.png
|
||||
design/icon/dark/apple-icon-57x57.png
|
||||
design/icon/dark/apple-icon-60x60.png
|
||||
design/icon/dark/apple-icon-72x72.png
|
||||
design/icon/dark/apple-icon-76x76.png
|
||||
design/icon/dark/apple-icon-precomposed.png
|
||||
design/icon/dark/apple-icon.png
|
||||
design/icon/dark/browserconfig.xml
|
||||
design/icon/dark/favicon-16x16.png
|
||||
design/icon/dark/favicon-32x32.png
|
||||
design/icon/dark/favicon-96x96.png
|
||||
design/icon/dark/favicon.ico
|
||||
design/icon/dark/manifest.json
|
||||
design/icon/dark/ms-icon-144x144.png
|
||||
design/icon/dark/ms-icon-150x150.png
|
||||
design/icon/dark/ms-icon-310x310.png
|
||||
design/icon/dark/ms-icon-70x70.png
|
||||
design/icon/light
|
||||
design/icon/light/android-icon-144x144.png
|
||||
design/icon/light/android-icon-192x192.png
|
||||
design/icon/light/android-icon-36x36.png
|
||||
design/icon/light/android-icon-48x48.png
|
||||
design/icon/light/android-icon-72x72.png
|
||||
design/icon/light/android-icon-96x96.png
|
||||
design/icon/light/apple-icon-114x114.png
|
||||
design/icon/light/apple-icon-120x120.png
|
||||
design/icon/light/apple-icon-144x144.png
|
||||
design/icon/light/apple-icon-152x152.png
|
||||
design/icon/light/apple-icon-180x180.png
|
||||
design/icon/light/apple-icon-57x57.png
|
||||
design/icon/light/apple-icon-60x60.png
|
||||
design/icon/light/apple-icon-72x72.png
|
||||
design/icon/light/apple-icon-76x76.png
|
||||
design/icon/light/apple-icon-precomposed.png
|
||||
design/icon/light/apple-icon.png
|
||||
design/icon/light/browserconfig.xml
|
||||
design/icon/light/favicon-16x16.png
|
||||
design/icon/light/favicon-32x32.png
|
||||
design/icon/light/favicon-96x96.png
|
||||
design/icon/light/favicon.ico
|
||||
design/icon/light/manifest.json
|
||||
design/icon/light/ms-icon-144x144.png
|
||||
design/icon/light/ms-icon-150x150.png
|
||||
|
||||
=== STACK DETECTION (manifest files) ===
|
||||
|
||||
--- Elsa.sln ---
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.7.34003.232
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{61017E64-6D00-49CB-9E81-5002DC8F7D5F}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
src\Directory.Build.props = src\Directory.Build.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{C6658DE0-2B2F-47F0-BB61-2CA66D435C09}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{7D21EB5E-D6B4-48A5-A9BC-A26387A65F0B}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
Directory.Build.props = Directory.Build.props
|
||||
Directory.Packages.props = Directory.Packages.props
|
||||
icon.png = icon.png
|
||||
NuGet.Config = NuGet.Config
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "doc", "doc", "{0354F050-3992-4DD4-B0EE-5FBA04AC72B6}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "modules", "modules", "{5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{90031D64-CA0F-46D0-9AF4-8DC023A5FFCD}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
test\Directory.Build.props = test\Directory.Build.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Workflows.Api", "src\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj", "{C9539BD8-D2AE-4A8D-8281-71A05B3FBF31}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Workflows.Core", "src\modules\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj", "{169E2C9B-6687-427F-A278-30BF849BFEDC}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Workflows.Management", "src\modules\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj", "{2DD5D66B-85E9-4AF9-911C-C9F963234159}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Workflows.Runtime", "src\modules\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj", "{8A050229-DB79-4E0B-9AFF-7565E87F2954}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Features", "src\common\Elsa.Features\Elsa.Features.csproj", "{C237BA1A-3A7D-4AB2-BE09-2696F3C082A4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Expressions", "src\modules\Elsa.Expressions\Elsa.Expressions.csproj", "{80529478-A383-4FEA-B744-C71264969E9A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Api.Common", "src\common\Elsa.Api.Common\Elsa.Api.Common.csproj", "{39CD855E-83B1-4A96-93F7-01608211EBE3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Identity", "src\modules\Elsa.Identity\Elsa.Identity.csproj", "{30B49E42-AE61-4F7C-981F-923F07BB3986}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Common", "src\modules\Elsa.Common\Elsa.Common.csproj", "{D229105F-6879-4452-9189-75DE060C0F4C}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E5482-0482-448C-B9E4-EC67A9474B85}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
docker\.dockerignore = docker\.dockerignore
|
||||
docker\docker-compose.yml = docker\docker-compose.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "unit", "unit", "{18453B51-25EB-4317-A4B3-B10518252E92}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
test\unit\Directory.Build.props = test\unit\Directory.Build.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "integration", "integration", "{1B8D5897-902E-4632-8698-E89CAF3DDF54}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
test\integration\Directory.Build.props = test\integration\Directory.Build.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "component", "component", "{08B41FFA-CEE3-46A7-B5C0-3EB65D37A16C}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
test\component\Directory.Build.props = test\component\Directory.Build.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Workflows.IntegrationTests", "test\integration\Elsa.Workflows.IntegrationTests\Elsa.Workflows.IntegrationTests.csproj", "{E9652738-2B3D-4357-B84B-54F0EA161382}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "clients", "clients", "{89608AA5-5ADE-4832-AC7B-871C4AE64210}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Api.Client", "src\clients\Elsa.Api.Client\Elsa.Api.Client.csproj", "{1FCB2200-28B8-4703-8E89-73241AAED047}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "expressions", "expressions", "{6EF07978-A6D2-40EB-891D-7D70C5F37E76}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "security", "security", "{5948B0A5-7873-4DBB-BA03-EB283D6EA91B}"
|
||||
|
||||
[TRUNCATED] Showing first 80 of 1336 lines.
|
||||
|
||||
=== ENTRY POINTS ===
|
||||
No common entry points found. Check 'main' or 'scripts.start' in manifest files above.
|
||||
|
||||
=== LINTING AND FORMATTING CONFIG ===
|
||||
Found: .editorconfig
|
||||
|
||||
=== ENVIRONMENT VARIABLE TEMPLATES ===
|
||||
No .env.example or .env.template found. Identify required environment variables by searching the code and config for environment variable reads.
|
||||
|
||||
=== TODO / FIXME / HACK (production code only, test dirs excluded) ===
|
||||
src/common/Elsa.Mediator/Middleware/Notification/Components/NotificationLoggingMiddleware.cs:20: // TODO: Log notification.
|
||||
src/modules/Elsa.Alterations/Services/DefaultAlterationRunner.cs:91: // TODO: Importing back into the workflow runtime makes sense, but this also causes another SAVE ction of the workflow instance in the DB, which also happens in the previous step during the commit action.
|
||||
src/modules/Elsa.Expressions.Liquid/Services/LiquidTemplateManager.cs:61: // TODO: add signal based cache invalidation.
|
||||
src/modules/Elsa.Http/Parsers/TextHtmlHttpContentParser.cs:8: // TODO: found a library to use a Html Content Parser and use a complexe object Type, until this, this class allow to accept request send using text/html content-type
|
||||
src/modules/Elsa.Http/DownloadableContentHandlers/UrlDownloadableContentHandler.cs:35: // TODO: Uncomment the next two lines if we implement file caching for this handler.
|
||||
src/modules/Elsa.Http/Activities/WriteFileHttpResponse.cs:153: // TODO: Delete the cached file after the workflow completes.
|
||||
src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs:704: // TODO: Check if we should not use the target subStatus here instead.
|
||||
src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs:150: // TODO: Disable filtering for now until we redesign log sanitization.
|
||||
src/modules/Elsa.Workflows.Core/Activities/ParallelForEachT.cs:73: // TODO: This should be configurable, because this won't work for e.g. file streams and other non-serializable types.
|
||||
src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs:79: // TODO: This is a temporary solution. We need to find a better way to handle this.
|
||||
src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs:96: // TODO: Remove this in the future when users have migrated workflows to use the new UsableAsActivity options property.
|
||||
src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs:105: // TODO: The following code handles backward compatibility with variable definitions referencing older .NET type namespaces.
|
||||
|
||||
=== GIT RECENT COMMITS (last 20) ===
|
||||
c7a97c641 Shorten configured application instance names
|
||||
0028604ca Merge pull request #7742 from elsa-workflows/sfmskywalker-port-asb-stable-instance-371
|
||||
1aef08381 fix: address greptile stable instance feedback
|
||||
fc48af062 fix: add stable application instance name option
|
||||
d8d7eae20 Merge branch 'release/3.6.2' into release/3.7.0
|
||||
a76e7a666 chore(packages): add Snappier
|
||||
b88af1e02 fix: handle arrays of generic types in GetFriendlyTypeName (issue - #7369) (#7400)
|
||||
8ee0b43b3 Add Claude Code GitHub Workflow (#7411)
|
||||
ad90e81af Enhance logging configuration, update HttpFeature dependencies, and add new identity persistence options
|
||||
e7eaa4b1f Integrate `Elsa.Workflows.Runtime.Distributed` into `Elsa.ModularServer.Web` and configure shell features for enhanced distributed runtime support.
|
||||
3d8d3b7de Merge remote-tracking branch 'origin/release/3.6.1'
|
||||
9f8423932 Handle null activities in `RunWorkflowResultAssertions`
|
||||
632d1a383 Refactor shells setup and clean up NuGet package source mapping.
|
||||
29d8a64a9 Update package versions: CShells to 0.0.14 and Nuplane to 0.0.1 in props file.
|
||||
6ff391745 Remove unused `Elsa.Workflows.Api.Contracts` import from `WorkflowsApiFeature`.
|
||||
3decb1268 feat: extend shells integration and modular server support (#7399)
|
||||
8d7d1a986 Fix BulkDispatchWorkflows sharing input dictionary across dispatches (#7284)
|
||||
a459904e1 refactor | Added a new property to expose the ability to disable the sorting (#7360)
|
||||
793ae15bf Fix tenant scope for interrupted workflow restarts (#7389)
|
||||
3a3766861 fix: correct workflow summary pagination on release 3.6.1 (#7392)
|
||||
|
||||
=== HIGH-CHURN FILES (last 90 days, top 20) ===
|
||||
4 Directory.Packages.props
|
||||
3 src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs
|
||||
3 test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs
|
||||
3 src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj
|
||||
3 src/apps/Elsa.ModularServer.Web/Program.cs
|
||||
2 src/modules/Elsa.Hosting.Management/Options/ApplicationInstanceOptions.cs
|
||||
2 Elsa.sln
|
||||
2 src/modules/Elsa.Common/Elsa.Common.csproj
|
||||
2 src/apps/Elsa.ModularServer.Web/appsettings.Development.json
|
||||
2 src/apps/Elsa.ModularServer.Web/appsettings.json
|
||||
2 NuGet.Config
|
||||
2 src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs
|
||||
2 README.md
|
||||
1 src/modules/Elsa.Hosting.Management/AssemblyInfo.cs
|
||||
1 src/modules/Elsa.Hosting.Management/Features/ClusteringFeature.cs
|
||||
1 test/unit/Elsa.Hosting.Management.UnitTests/Elsa.Hosting.Management.UnitTests.csproj
|
||||
1 src/modules/Elsa.Expressions/Extensions/TypeExtensions.cs
|
||||
1 test/unit/Elsa.Expressions.UnitTests/Extensions/TypeExtensionsTests.cs
|
||||
1 .github/workflows/claude-code-review.yml
|
||||
1 .github/workflows/claude.yml
|
||||
|
||||
=== MONOREPO SIGNALS ===
|
||||
No monorepo signals detected.
|
||||
|
||||
=== CODE METRICS ===
|
||||
Total files scanned: 3908
|
||||
Total lines of code: 217582
|
||||
|
||||
Files by language:
|
||||
C#: 3265
|
||||
Other: 631
|
||||
Java: 6
|
||||
JavaScript: 4
|
||||
Python: 2
|
||||
|
||||
Top 10 largest files:
|
||||
design/artwork/elsa-logo-art.psd: 13979.6KB
|
||||
src/apps/Elsa.ModularServer.Web/elsa_logs.db: 9796.0KB
|
||||
design/artwork/elsa-v3-avatar.png: 5827.9KB
|
||||
design/artwork/creative-elsa-animation.gif: 3461.2KB
|
||||
design/artwork/elsa-waves.mp4: 3178.4KB
|
||||
design/logo/Esla_Source.ai: 2043.9KB
|
||||
src/apps/Elsa.ModularServer.Web/console-log-streaming-vanilla-sample.db: 1532.0KB
|
||||
src/apps/Elsa.ModularServer.Web/elsa_workflows.db: 1416.0KB
|
||||
design/logo/Elsa.ai: 914.8KB
|
||||
design/logo/Esla-13.png: 429.9KB
|
||||
|
||||
=== CI/CD PIPELINES ===
|
||||
CI/CD: GitHub Actions
|
||||
|
||||
=== CONTAINERS & ORCHESTRATION ===
|
||||
No containerization configs detected.
|
||||
|
||||
=== SECURITY & COMPLIANCE ===
|
||||
No security configs detected.
|
||||
|
||||
=== PERFORMANCE & TESTING ===
|
||||
No performance testing configs detected.
|
||||
|
||||
=== SCAN COMPLETE ===
|
||||
304
jordi-delay-bookmark-reply.md
Normal file
304
jordi-delay-bookmark-reply.md
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
# Reply to Jordi - Delay bookmarks and manual recovery
|
||||
|
||||
Good morning Jordi,
|
||||
|
||||
Thanks for the detailed explanation.
|
||||
|
||||
Before we go too deep into the recovery options, can you please confirm the exact Elsa version you are running? You can usually check this with:
|
||||
|
||||
```http
|
||||
GET /elsa/api/package/version
|
||||
```
|
||||
|
||||
Alternatively, please send us the Elsa NuGet package versions used by the application.
|
||||
|
||||
## Short answer
|
||||
|
||||
The endpoint you are currently calling is not the right one for a Delay activity bookmark:
|
||||
|
||||
```http
|
||||
POST /elsa/api/events/{eventName}/trigger
|
||||
```
|
||||
|
||||
That endpoint is for Elsa `Event` activities. It publishes an event stimulus. A `200 OK` response means that the event request was accepted, but it does not mean that a Delay bookmark was matched or resumed.
|
||||
|
||||
For a `Delay` activity, Elsa stores a bookmark named:
|
||||
|
||||
```text
|
||||
Elsa.Delay
|
||||
```
|
||||
|
||||
The bookmark is matched by bookmark id, workflow instance id, activity instance id, and/or the generated bookmark hash. It is not resumed by using the designer activity name such as `Delay8` or `Delay.8`.
|
||||
|
||||
Normally, Quartz should resume this Delay automatically. Quartz schedules a task that resumes the workflow instance using the stored `WorkflowInstanceId` and `BookmarkId`.
|
||||
|
||||
## Recommended first step: reproduce or inspect the stuck case
|
||||
|
||||
The best next step is to determine why Quartz did not resume the Delay bookmark. If we can reproduce the issue, we can fix the root cause instead of only providing a manual workaround.
|
||||
|
||||
For one stuck instance, please capture:
|
||||
|
||||
- Elsa version.
|
||||
- Workflow instance id.
|
||||
- Bookmark id.
|
||||
- Activity instance id of the Delay activity.
|
||||
- The `Elsa.Bookmarks` row for that workflow instance.
|
||||
- The matching Quartz job/trigger row.
|
||||
- Logs around the expected Delay resume time.
|
||||
- Whether the workflow instance is still `Running`, or whether it was already cancelled/faulted/finished.
|
||||
|
||||
For example, for a stuck Delay bookmark, the relevant values would look like:
|
||||
|
||||
```text
|
||||
BookmarkId: <bookmark-id>
|
||||
WorkflowInstanceId: <workflow-instance-id>
|
||||
ActivityInstanceId: <delay-activity-instance-id>
|
||||
Bookmark name: Elsa.Delay
|
||||
```
|
||||
|
||||
## How to manually resume a stuck Delay safely
|
||||
|
||||
I would not recommend directly updating workflow instance state in the database. Resuming a workflow is not just a database state change. The Elsa runtime has to:
|
||||
|
||||
- Load the workflow instance.
|
||||
- Resume the correct bookmarked activity.
|
||||
- Remove the consumed bookmark.
|
||||
- Persist the new workflow state.
|
||||
- Save variables.
|
||||
- Write execution logs.
|
||||
- Emit the notifications that scheduling cleanup depends on.
|
||||
|
||||
The safest manual recovery option is to expose a temporary authenticated admin endpoint in your Elsa server that calls Elsa's runtime service `IWorkflowResumer`.
|
||||
|
||||
Example:
|
||||
|
||||
```csharp
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
[ApiController]
|
||||
[Route("admin/elsa/bookmarks")]
|
||||
[Authorize] // Restrict this further in production.
|
||||
public class ElsaBookmarkAdminController : ControllerBase
|
||||
{
|
||||
[HttpPost("{bookmarkId}/resume")]
|
||||
public async Task<IActionResult> Resume(
|
||||
string bookmarkId,
|
||||
[FromBody] ResumeBookmarkAdminRequest request,
|
||||
[FromServices] IWorkflowResumer workflowResumer,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var responses = await workflowResumer.ResumeAsync(new ResumeBookmarkRequest
|
||||
{
|
||||
BookmarkId = bookmarkId,
|
||||
WorkflowInstanceId = request.WorkflowInstanceId,
|
||||
ActivityInstanceId = request.ActivityInstanceId,
|
||||
Input = request.Input
|
||||
}, cancellationToken);
|
||||
|
||||
var responseList = responses.ToList();
|
||||
|
||||
if (responseList.Count == 0)
|
||||
return NotFound(new
|
||||
{
|
||||
message = "No matching bookmark was found or resumed.",
|
||||
bookmarkId,
|
||||
request.WorkflowInstanceId,
|
||||
request.ActivityInstanceId
|
||||
});
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
resumed = responseList.Count,
|
||||
responses = responseList
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class ResumeBookmarkAdminRequest
|
||||
{
|
||||
public string WorkflowInstanceId { get; set; } = default!;
|
||||
public string? ActivityInstanceId { get; set; }
|
||||
public IDictionary<string, object>? Input { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
Example request:
|
||||
|
||||
```http
|
||||
POST /admin/elsa/bookmarks/2cdf1537300ebbde/resume
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"workflowInstanceId": "2f95829eb7abe851",
|
||||
"activityInstanceId": "2aacb800a50923f4"
|
||||
}
|
||||
```
|
||||
|
||||
If the bookmark is still valid and the workflow instance is still running, this should resume the Delay through the Elsa runtime.
|
||||
|
||||
## Why not use `/events/{eventName}/trigger`?
|
||||
|
||||
This endpoint resumes workflows waiting on an Elsa `Event` activity. Internally, it publishes an event stimulus.
|
||||
|
||||
A Delay bookmark uses the scheduling stimulus name:
|
||||
|
||||
```text
|
||||
Elsa.Delay
|
||||
```
|
||||
|
||||
and its payload contains the `resumeAt` timestamp. So calling:
|
||||
|
||||
```http
|
||||
POST /elsa/api/events/Delay8/trigger
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```http
|
||||
POST /elsa/api/events/Delay.8/trigger
|
||||
```
|
||||
|
||||
will not match a Delay bookmark.
|
||||
|
||||
## Built-in bookmark resume endpoint
|
||||
|
||||
Elsa also has a built-in bookmark resume endpoint:
|
||||
|
||||
```http
|
||||
POST /elsa/api/bookmarks/resume?t={token}
|
||||
```
|
||||
|
||||
However, this endpoint expects a signed bookmark token, not just the bookmark id from the database. It is useful when a workflow generated a bookmark resume URL/token, but it is usually not the practical recovery path for an internal Delay activity that was scheduled by Quartz.
|
||||
|
||||
For this scenario, the small admin endpoint above is more appropriate.
|
||||
|
||||
## If you still want a database-level fallback
|
||||
|
||||
If you absolutely need a database-level operation, I would avoid editing these directly:
|
||||
|
||||
- `Elsa.WorkflowInstances`
|
||||
- `Elsa.Bookmarks`
|
||||
- Quartz trigger state, unless we are only cleaning up after a cancelled/deleted instance
|
||||
|
||||
Changing those manually can leave the workflow state, runtime bookmarks, variables, logs, and scheduler state inconsistent.
|
||||
|
||||
The least invasive database-style nudge is to insert a bookmark queue item and let Elsa's bookmark queue processor resume it. This still requires the Elsa application to be running and processing bookmark queue items.
|
||||
|
||||
Important: the exact table and column names can vary by Elsa version and provider, so please confirm your version before using SQL like this.
|
||||
|
||||
Illustrative SQL Server example:
|
||||
|
||||
```sql
|
||||
INSERT INTO Elsa.BookmarkQueueItems
|
||||
(
|
||||
Id,
|
||||
WorkflowInstanceId,
|
||||
BookmarkId,
|
||||
ActivityInstanceId,
|
||||
ActivityTypeName,
|
||||
CreatedAt,
|
||||
SerializedOptions,
|
||||
TenantId
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
LOWER(REPLACE(CONVERT(varchar(36), NEWID()), '-', '')),
|
||||
'2f95829eb7abe851',
|
||||
'2cdf1537300ebbde',
|
||||
'2aacb800a50923f4',
|
||||
'Elsa.Delay',
|
||||
SYSUTCDATETIME(),
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- This does not directly resume the workflow.
|
||||
- It only creates work for Elsa's bookmark queue processor.
|
||||
- If the queue processor is not running, nothing will happen.
|
||||
- A direct DB insert bypasses Elsa's in-process queue signaler, so you may need to wait for the recurring queue worker or restart the Elsa application.
|
||||
- This is still less preferable than calling an application endpoint that invokes `IWorkflowResumer` or `IBookmarkQueue`.
|
||||
|
||||
If you want a slightly safer variant of this queue approach, expose an admin endpoint that enqueues the bookmark through Elsa instead of inserting into the database manually:
|
||||
|
||||
```csharp
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
[ApiController]
|
||||
[Route("admin/elsa/bookmark-queue")]
|
||||
[Authorize]
|
||||
public class ElsaBookmarkQueueAdminController : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Enqueue(
|
||||
[FromBody] EnqueueBookmarkRequest request,
|
||||
[FromServices] IBookmarkQueue bookmarkQueue,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await bookmarkQueue.EnqueueAsync(new NewBookmarkQueueItem
|
||||
{
|
||||
WorkflowInstanceId = request.WorkflowInstanceId,
|
||||
BookmarkId = request.BookmarkId,
|
||||
ActivityInstanceId = request.ActivityInstanceId,
|
||||
ActivityTypeName = "Elsa.Delay"
|
||||
}, cancellationToken);
|
||||
|
||||
return Accepted();
|
||||
}
|
||||
}
|
||||
|
||||
public class EnqueueBookmarkRequest
|
||||
{
|
||||
public string WorkflowInstanceId { get; set; } = default!;
|
||||
public string BookmarkId { get; set; } = default!;
|
||||
public string? ActivityInstanceId { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
That endpoint has the advantage that it uses Elsa's queue abstraction and triggers the queue processor.
|
||||
|
||||
## About cancelled instances and Quartz rows
|
||||
|
||||
If a workflow instance is cancelled, it should not resume anymore. Any remaining Delay bookmark or Quartz job for that instance is stale data.
|
||||
|
||||
In the normal flow, when bookmarks are deleted through Elsa, the scheduling layer is notified and the corresponding scheduled jobs are unscheduled. However, whether cancellation also removes the persisted bookmark and Quartz job in your exact setup depends on the Elsa version and the cancellation path being used.
|
||||
|
||||
That is why I would like to verify your version before giving a definitive answer here.
|
||||
|
||||
If cancelled instances still leave `Elsa.Delay` bookmarks or Quartz jobs behind, please send one example and we can check whether:
|
||||
|
||||
- this is already fixed in a newer Elsa version;
|
||||
- the cancellation path is bypassing the bookmark cleanup flow;
|
||||
- the Quartz job is stale and can be cleaned safely;
|
||||
- or we need to add a cleanup/fix.
|
||||
|
||||
## About the 3-second Delay workaround
|
||||
|
||||
The 3-second Delay before the child workflow finishes sounds like a workaround for a race/timing issue in how values are propagated back to the parent workflow.
|
||||
|
||||
Variable propagation from child to parent should not depend on sleeping. Once I know your Elsa version and can see a minimal reproduction, I can check whether this is a known issue, a fixed issue, or whether there is a better pattern for returning values from the child workflow to the parent.
|
||||
|
||||
## Suggested recovery order
|
||||
|
||||
For the current stuck instances, I suggest this order:
|
||||
|
||||
1. Confirm the Elsa version with `GET /elsa/api/package/version`.
|
||||
2. Pick one stuck workflow instance as an example.
|
||||
3. Verify the workflow instance is still `Running`.
|
||||
4. Verify the `Elsa.Bookmarks` row exists and has `Name = 'Elsa.Delay'`.
|
||||
5. Verify the Quartz job/trigger exists for the same bookmark id.
|
||||
6. Check logs around the expected resume time.
|
||||
7. Try manual resume through the temporary `IWorkflowResumer` admin endpoint.
|
||||
8. If that works, we know the workflow itself can continue and the problem is likely in scheduling/Quartz execution.
|
||||
9. If it does not work, capture the response/logs because the bookmark may no longer match the workflow state, or the instance may no longer be resumable.
|
||||
10. Only use database queue insertion as a last resort, and only after confirming the exact Elsa version/schema.
|
||||
|
||||
Best regards,
|
||||
|
||||
Sipke
|
||||
181
pau-delay-dispatch-response.md
Normal file
181
pau-delay-dispatch-response.md
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
# Draft response to Pau
|
||||
|
||||
Hi Pau,
|
||||
|
||||
Thanks for the detailed information and screenshots. The confirmed Elsa version `3.6.1` is very helpful.
|
||||
|
||||
## TL;DR
|
||||
|
||||
- We started a focused `3.6.1` reproduction on our side.
|
||||
- So far, seeing only `Elsa.Delay` in the active `Bookmarks` table after the parent has passed `Dispatch Workflow` looks expected.
|
||||
- The current active blocker appears to be the Delay bookmark, so the next checks are Delay payload, Quartz trigger/job, logs, and workflow instance state.
|
||||
- Please send the smallest Studio export that reproduces the issue.
|
||||
- If possible, configure that reproduction with SQLite and include the SQLite database file captured after the workflow gets stuck.
|
||||
|
||||
We have also started a focused `3.6.1` reproduction on our side, using a small parent/child workflow with `Dispatch Workflow`, `Wait for Completion`, and a subsequent `Delay`. So far, the active bookmark table containing only `Elsa.Delay` after the parent continues past `Dispatch Workflow` looks expected. Once the dispatch bookmark has resumed the parent, it should be consumed and disappear from the active `Bookmarks` table.
|
||||
|
||||
At the same time, from what you sent, I do not yet see enough to reproduce your exact scenario end to end. The fastest route for us to investigate this properly, and to fix the full issue if it is a bug, would still be for you to send us a minimal reproduction exported from Elsa Studio: the smallest set of workflow definitions that still reproduces the issue.
|
||||
|
||||
Ideally this would include:
|
||||
|
||||
- The parent workflow exported from Studio.
|
||||
- The child workflow exported from Studio.
|
||||
- Only the minimum activities required to reproduce the problem.
|
||||
- The exact configuration of `Dispatch Workflow` / `Wait for Completion`.
|
||||
- The final `Delay` activity.
|
||||
- Any variables/outputs involved in passing data from the child workflow back to the parent.
|
||||
- The approximate reproduction rate, for example “1 in 10 executions” or “always when run under load”.
|
||||
- If possible, a reproduction configured against SQLite, with the SQLite database file included after the issue occurs. That would let us inspect the workflow definitions, workflow instances, bookmarks, queue items, execution logs, and persisted state exactly as they are after the failure.
|
||||
|
||||
Without that, we can investigate the general mechanism and likely failure modes, but we may still miss a detail that only exists in your actual workflow structure, variable/output configuration, persistence setup, or Quartz configuration.
|
||||
|
||||
## What I think is happening
|
||||
|
||||
In Elsa `3.6.1`, `Dispatch Workflow` with `Wait for Completion` works roughly like this:
|
||||
|
||||
1. The parent dispatches the child workflow.
|
||||
2. The parent creates a bookmark named `Elsa.DispatchWorkflow`.
|
||||
3. When the child workflow finishes, Elsa enqueues a bookmark queue item for the parent.
|
||||
4. The bookmark queue processor resumes the parent using the `Elsa.DispatchWorkflow` bookmark.
|
||||
5. The parent continues execution.
|
||||
6. If the parent later reaches a `Delay`, Elsa creates a new bookmark named `Elsa.Delay`.
|
||||
|
||||
Because of that, if the parent has already continued past `Dispatch Workflow` and then stopped at the `Delay`, it is expected that the active bookmark table contains only the `Elsa.Delay` bookmark. In that state, the `Elsa.DispatchWorkflow` bookmark should normally no longer be present in the active `Bookmarks` table because it has already been consumed.
|
||||
|
||||
So the database result showing only `Elsa.Delay` for the parent instance is not necessarily wrong. It can mean that `Wait for Completion` did resume correctly, and the current active blocker is the `Delay`.
|
||||
|
||||
The part I would like to verify is the workflow state view that still shows `Elsa.DispatchWorkflow`. If that is the latest persisted state for the same parent instance, then we may have an inconsistency between the serialized workflow state and the active bookmark index. If it is an older state snapshot, a cached UI view, or a different instance, then the database result may be the more accurate indicator of the current active bookmark.
|
||||
|
||||
## How to determine if the workflow is actually stuck
|
||||
|
||||
For a given parent workflow instance, please check these together:
|
||||
|
||||
1. `WorkflowInstances`: status/sub-status.
|
||||
2. `Bookmarks`: active bookmarks for that workflow instance.
|
||||
3. `BookmarkQueueItems`: pending resume requests for that workflow instance.
|
||||
4. `WorkflowExecutionLogRecords`: last execution messages for that workflow instance.
|
||||
5. `ActivityExecutionRecords`: latest activity records, especially `Elsa.DispatchWorkflow` and `Elsa.Delay`.
|
||||
6. Quartz job/trigger tables: scheduled job for the active `Elsa.Delay` bookmark ID.
|
||||
|
||||
In the state JSON from your screenshot, `Status = 0` means `Running` and `SubStatus = 2` means `Suspended`. That is normal for a workflow waiting on a bookmark. A workflow is “stuck” only if it is suspended on a bookmark that should have been resumed already, for example a Delay whose `ResumeAt` is in the past and whose Quartz trigger has not fired or cannot resume the instance.
|
||||
|
||||
## What database tables to inspect
|
||||
|
||||
For one affected parent workflow instance ID, please inspect:
|
||||
|
||||
```sql
|
||||
-- Active bookmarks for the parent instance.
|
||||
select *
|
||||
from [Elsa].[Bookmarks]
|
||||
where [WorkflowInstanceId] = '<parent-workflow-instance-id>'
|
||||
order by [CreatedAt] desc;
|
||||
|
||||
-- Pending bookmark queue items for the parent instance.
|
||||
select *
|
||||
from [Elsa].[BookmarkQueueItems]
|
||||
where [WorkflowInstanceId] = '<parent-workflow-instance-id>'
|
||||
order by [CreatedAt] desc;
|
||||
|
||||
-- Activity execution records around DispatchWorkflow and Delay.
|
||||
select *
|
||||
from [Elsa].[ActivityExecutionRecords]
|
||||
where [WorkflowInstanceId] = '<parent-workflow-instance-id>'
|
||||
order by [StartedAt] desc;
|
||||
|
||||
-- Workflow execution log.
|
||||
select *
|
||||
from [Elsa].[WorkflowExecutionLogRecords]
|
||||
where [WorkflowInstanceId] = '<parent-workflow-instance-id>'
|
||||
order by [Timestamp] desc, [Sequence] desc;
|
||||
```
|
||||
|
||||
Also inspect the workflow instance row itself and confirm that the workflow state shown in the UI belongs to the same parent instance ID that you used in the bookmark query.
|
||||
|
||||
For Quartz, check whether there is a job/trigger for the active `Elsa.Delay` bookmark ID. The Delay bookmark ID should be the scheduler task/job key Elsa uses for that Delay.
|
||||
|
||||
## Logs to enable
|
||||
|
||||
For a short reproduction window, I suggest:
|
||||
|
||||
```json
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Elsa": "Debug",
|
||||
"Elsa.Workflows.Runtime": "Debug",
|
||||
"Elsa.Scheduling": "Debug",
|
||||
"Quartz": "Debug",
|
||||
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If the logs are too noisy, keep `Elsa.Workflows.Runtime`, `Elsa.Scheduling`, and `Quartz` at `Debug`, and keep the rest at `Information`.
|
||||
|
||||
The most useful log points are:
|
||||
|
||||
- Creation of the `Elsa.DispatchWorkflow` bookmark.
|
||||
- Enqueueing of the bookmark queue item when the child workflow finishes.
|
||||
- Processing of `BookmarkQueueItems`.
|
||||
- Successful resume of the parent workflow.
|
||||
- Creation of the `Elsa.Delay` bookmark.
|
||||
- Scheduling of the Delay bookmark in Quartz.
|
||||
- Quartz firing the Delay job.
|
||||
- Resume attempt for the Delay bookmark.
|
||||
|
||||
## About a possible race condition
|
||||
|
||||
Yes, this area is timing-sensitive by nature: the child can finish before or around the time the parent has committed its `Elsa.DispatchWorkflow` bookmark.
|
||||
|
||||
However, Elsa `3.6.1` has a bookmark queue mechanism intended to handle exactly this kind of race. When the child finishes, Elsa does not require the parent bookmark to already exist at that exact millisecond. It enqueues a bookmark queue item, and the queue processor retries matching it against the bookmark store.
|
||||
|
||||
That means the first thing I would check is not only the `Bookmarks` table, but also `BookmarkQueueItems`. If old `Elsa.DispatchWorkflow` queue items remain there, the parent resume request may not be matching the bookmark. If there are no old dispatch queue items and the parent is currently waiting on `Elsa.Delay`, then the dispatch/wait part probably completed and the issue is more likely in Delay/Quartz resume.
|
||||
|
||||
## Could Delay be related?
|
||||
|
||||
Yes, but based on the screenshots it may be the current active blocker rather than the cause of the dispatch bookmark issue.
|
||||
|
||||
If the parent reaches `Delay`, creates an `Elsa.Delay` bookmark, and then never resumes, the key questions are:
|
||||
|
||||
- Does the `Elsa.Delay` bookmark payload contain a `ResumeAt` time that is already in the past?
|
||||
- Does Quartz have a trigger for that exact bookmark ID?
|
||||
- Did Quartz fire the trigger?
|
||||
- Did Elsa receive/process the resume request?
|
||||
- Did the resume fail because the instance was no longer `Running`, was faulted, or had a bookmark mismatch?
|
||||
|
||||
## What you can do to minimize impact
|
||||
|
||||
Until we have a reproduction, the practical mitigations are:
|
||||
|
||||
1. Monitor `BookmarkQueueItems` for old rows. Old rows can indicate resume requests that never matched a bookmark.
|
||||
2. Monitor `Bookmarks` for `Elsa.Delay` rows whose resume time is in the past.
|
||||
3. Monitor Quartz triggers for Delay jobs that should already have fired.
|
||||
4. Add temporary diagnostic logging around Elsa runtime, scheduling, and Quartz.
|
||||
5. Avoid relying on the 3-second Delay as a synchronization mechanism if possible; if output propagation needs a delay to work reliably, that is a separate symptom we should include in the reproduction.
|
||||
|
||||
## What I need from you next
|
||||
|
||||
Please send a minimal Studio export that reproduces the issue. If possible, include:
|
||||
|
||||
1. Parent workflow.
|
||||
2. Child workflow.
|
||||
3. Any required variables and outputs.
|
||||
4. Steps to run it.
|
||||
5. Expected behavior.
|
||||
6. Actual behavior.
|
||||
7. Preferably, a SQLite-backed reproduction database captured after the workflow gets stuck.
|
||||
8. One affected parent workflow instance ID with matching rows from:
|
||||
- `Bookmarks`
|
||||
- `BookmarkQueueItems`
|
||||
- `WorkflowInstances`
|
||||
- `ActivityExecutionRecords`
|
||||
- `WorkflowExecutionLogRecords`
|
||||
- Quartz trigger/job tables
|
||||
|
||||
Once we can reproduce it locally, we can determine whether this is a configuration issue, a stale Quartz schedule, a bookmark queue issue, or a bug in Elsa `3.6.1`.
|
||||
|
||||
Best regards,
|
||||
|
||||
Sipke
|
||||
|
|
@ -80,164 +80,7 @@
|
|||
"DefaultType": "reject",
|
||||
"AllowDatabaseConnectionOverride": true
|
||||
},
|
||||
"Connections": [
|
||||
{
|
||||
"Id": "keycloak-idp-configuration",
|
||||
"Key": "keycloak-idp",
|
||||
"AdapterType": "openid-connect",
|
||||
"AdapterSettingsVersion": 2,
|
||||
"AdapterSettings": {
|
||||
"mode": "discovery",
|
||||
"discoveryUrl": "https://localhost:8443/realms/elsa/.well-known/openid-configuration",
|
||||
"clientId": "elsa-studio-idp",
|
||||
"clientAuthenticationMethod": "client_secret_basic",
|
||||
"scopes": [
|
||||
"profile",
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"SecretBindings": {
|
||||
"clientSecret": {
|
||||
"Ownership": "External",
|
||||
"ResolverType": "configuration",
|
||||
"Reference": "ExternalAuthentication:Secrets:KeycloakGeneralClientSecret",
|
||||
"ExpectedType": "text",
|
||||
"ExpectedScope": "external-authentication"
|
||||
}
|
||||
},
|
||||
"DisplayName": "Keycloak General",
|
||||
"DisplayOrder": 10,
|
||||
"IsPreferred": true,
|
||||
"IsEnabled": true,
|
||||
"UnlinkedPolicy": {
|
||||
"Type": "create-user",
|
||||
"SettingsVersion": 1,
|
||||
"Settings": {
|
||||
"defaultRoleIds": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ClaimProjection": {
|
||||
"AllowedClaimTypes": [
|
||||
"preferred_username",
|
||||
"name",
|
||||
"given_name",
|
||||
"family_name",
|
||||
"email"
|
||||
],
|
||||
"RedactedClaimTypes": [],
|
||||
"MaximumClaimCount": 64,
|
||||
"MaximumValueLength": 1024,
|
||||
"MaximumTotalBytes": 16384
|
||||
},
|
||||
"UpstreamLogoutMode": "UserChoice"
|
||||
},
|
||||
{
|
||||
"Id": "keycloak-workforce-configuration",
|
||||
"Key": "keycloak-workforce",
|
||||
"AdapterType": "openid-connect",
|
||||
"AdapterSettingsVersion": 2,
|
||||
"AdapterSettings": {
|
||||
"mode": "discovery",
|
||||
"discoveryUrl": "https://localhost:8443/realms/elsa-workforce/.well-known/openid-configuration",
|
||||
"clientId": "elsa-workforce-idp",
|
||||
"clientAuthenticationMethod": "client_secret_basic",
|
||||
"scopes": [
|
||||
"profile",
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"SecretBindings": {
|
||||
"clientSecret": {
|
||||
"Ownership": "External",
|
||||
"ResolverType": "configuration",
|
||||
"Reference": "ExternalAuthentication:Secrets:KeycloakWorkforceClientSecret",
|
||||
"ExpectedType": "text",
|
||||
"ExpectedScope": "external-authentication"
|
||||
}
|
||||
},
|
||||
"DisplayName": "Keycloak Workforce",
|
||||
"DisplayOrder": 20,
|
||||
"IsPreferred": false,
|
||||
"IsEnabled": true,
|
||||
"UnlinkedPolicy": {
|
||||
"Type": "create-user",
|
||||
"SettingsVersion": 1,
|
||||
"Settings": {
|
||||
"defaultRoleIds": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ClaimProjection": {
|
||||
"AllowedClaimTypes": [
|
||||
"preferred_username",
|
||||
"name",
|
||||
"given_name",
|
||||
"family_name",
|
||||
"email"
|
||||
],
|
||||
"RedactedClaimTypes": [],
|
||||
"MaximumClaimCount": 64,
|
||||
"MaximumValueLength": 1024,
|
||||
"MaximumTotalBytes": 16384
|
||||
},
|
||||
"UpstreamLogoutMode": "UserChoice"
|
||||
},
|
||||
{
|
||||
"Id": "keycloak-partners-configuration",
|
||||
"Key": "keycloak-partners",
|
||||
"AdapterType": "openid-connect",
|
||||
"AdapterSettingsVersion": 2,
|
||||
"AdapterSettings": {
|
||||
"mode": "discovery",
|
||||
"discoveryUrl": "https://localhost:8443/realms/elsa-partners/.well-known/openid-configuration",
|
||||
"clientId": "elsa-partners-idp",
|
||||
"clientAuthenticationMethod": "client_secret_basic",
|
||||
"scopes": [
|
||||
"profile",
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"SecretBindings": {
|
||||
"clientSecret": {
|
||||
"Ownership": "External",
|
||||
"ResolverType": "configuration",
|
||||
"Reference": "ExternalAuthentication:Secrets:KeycloakPartnersClientSecret",
|
||||
"ExpectedType": "text",
|
||||
"ExpectedScope": "external-authentication"
|
||||
}
|
||||
},
|
||||
"DisplayName": "Keycloak Partners",
|
||||
"DisplayOrder": 30,
|
||||
"IsPreferred": false,
|
||||
"IsEnabled": true,
|
||||
"UnlinkedPolicy": {
|
||||
"Type": "create-user",
|
||||
"SettingsVersion": 1,
|
||||
"Settings": {
|
||||
"defaultRoleIds": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ClaimProjection": {
|
||||
"AllowedClaimTypes": [
|
||||
"preferred_username",
|
||||
"name",
|
||||
"given_name",
|
||||
"family_name",
|
||||
"email"
|
||||
],
|
||||
"RedactedClaimTypes": [],
|
||||
"MaximumClaimCount": 64,
|
||||
"MaximumValueLength": 1024,
|
||||
"MaximumTotalBytes": 16384
|
||||
},
|
||||
"UpstreamLogoutMode": "UserChoice"
|
||||
}
|
||||
],
|
||||
"Connections": [],
|
||||
"AuthenticationClients": [
|
||||
{
|
||||
"ClientId": "elsa-studio-server",
|
||||
|
|
|
|||
3
src/modules/Elsa.Hosting.Management/AssemblyInfo.cs
Normal file
3
src/modules/Elsa.Hosting.Management/AssemblyInfo.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Elsa.Hosting.Management.UnitTests")]
|
||||
|
|
@ -72,13 +72,28 @@ public class ConfiguredApplicationInstanceNameProvider : IApplicationInstanceNam
|
|||
private static string ResolveConfiguredInstanceName(string value, string source, ILogger logger)
|
||||
{
|
||||
var instanceName = value.Trim();
|
||||
var isTooLong = instanceName.Length > ConfiguredInstanceNameMaxLength;
|
||||
var hasInvalidCharacters = !IsValidConfiguredInstanceName(instanceName);
|
||||
|
||||
if (!IsValidConfiguredInstanceName(instanceName))
|
||||
throw new InvalidOperationException(
|
||||
if (hasInvalidCharacters)
|
||||
{
|
||||
var errors = new List<string>
|
||||
{
|
||||
$"The configured application instance name from {source} contains invalid characters. " +
|
||||
"Use only letters, numbers, periods, hyphens, or underscores, and start and end the value with a letter or number.");
|
||||
"Use only letters, numbers, periods, hyphens, or underscores, and start and end the value with a letter or number."
|
||||
};
|
||||
|
||||
if (instanceName.Length <= ConfiguredInstanceNameMaxLength)
|
||||
if (isTooLong)
|
||||
{
|
||||
errors.Add(
|
||||
$"The configured application instance name from {source} is {instanceName.Length} characters long, but it must be {ConfiguredInstanceNameMaxLength} characters or fewer. " +
|
||||
$"The value is used to create per-instance transport entities such as '{instanceName}{TriggerChangeTokenSignalEndpointNameSuffix}', which must fit within Azure Service Bus's {AzureServiceBusSubscriptionNameMaxLength}-character subscription name limit.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(string.Join(" ", errors));
|
||||
}
|
||||
|
||||
if (!isTooLong)
|
||||
return instanceName;
|
||||
|
||||
var shortenedName = ShortenConfiguredInstanceName(instanceName);
|
||||
|
|
@ -96,7 +111,8 @@ public class ConfiguredApplicationInstanceNameProvider : IApplicationInstanceNam
|
|||
|
||||
private static bool IsValidConfiguredInstanceName(string instanceName)
|
||||
{
|
||||
return IsAsciiLetterOrDigit(instanceName[0])
|
||||
return instanceName.Length > 0
|
||||
&& IsAsciiLetterOrDigit(instanceName[0])
|
||||
&& IsAsciiLetterOrDigit(instanceName[^1])
|
||||
&& instanceName.All(c => IsAsciiLetterOrDigit(c) || c is '.' or '-' or '_');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
using Elsa.Expressions.Helpers;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Http.Webhooks.Stimuli;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Attributes;
|
||||
using Elsa.Workflows.Models;
|
||||
using WebhooksCore;
|
||||
|
||||
namespace Elsa.Http.Webhooks.Activities;
|
||||
|
||||
public class WebhookEventReceived : Trigger<WebhookEvent>, INotification
|
||||
{
|
||||
/// <summary>
|
||||
/// The webhook event type to be received.
|
||||
/// </summary>
|
||||
[Input(Description = "The webhook event type to be received.")]
|
||||
public string EventType { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The webhook event payload type to be received.
|
||||
/// </summary>
|
||||
[Input(Description = "The webhook event payload type to be received.")]
|
||||
public Type? PayloadType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The webhook payload, if any.
|
||||
/// </summary>
|
||||
[Output(Description = "The webhook payload, if any.")]
|
||||
public Output<object?> Payload { get; set; } = null!;
|
||||
|
||||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
if (!context.IsTriggerOfWorkflow())
|
||||
{
|
||||
context.CreateBookmark(GetStimulus(context.ExpressionExecutionContext), OnResumeAsync);
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteInternalAsync(context);
|
||||
}
|
||||
|
||||
protected override object GetTriggerPayload(TriggerIndexingContext context)
|
||||
{
|
||||
return new WebhookEventReceivedStimulus(EventType);
|
||||
}
|
||||
|
||||
private async ValueTask OnResumeAsync(ActivityExecutionContext context)
|
||||
{
|
||||
await ExecuteInternalAsync(context);
|
||||
}
|
||||
|
||||
private object GetStimulus(ExpressionExecutionContext context)
|
||||
{
|
||||
return new WebhookEventReceivedStimulus(EventType);
|
||||
}
|
||||
|
||||
private async Task ExecuteInternalAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var receivedWebhookEvent = context.GetWorkflowInput<WebhookEvent>();
|
||||
var payload = receivedWebhookEvent.Payload;
|
||||
var payloadType = PayloadType;
|
||||
|
||||
if (payloadType != null && payload != null)
|
||||
{
|
||||
var deserializedPayload = payload.ConvertTo(payloadType);
|
||||
payload = deserializedPayload;
|
||||
}
|
||||
|
||||
context.SetResult(receivedWebhookEvent);
|
||||
Payload.Set(context, payload);
|
||||
await context.CompleteActivityAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
using Elsa.Http.Webhooks.Activities;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Models;
|
||||
using WebhooksCore;
|
||||
|
||||
namespace Elsa.Http.Webhooks.ActivityProviders;
|
||||
|
||||
/// <summary>
|
||||
/// An activity provider that generates activity types based on webhook sources.
|
||||
/// </summary>
|
||||
public class WebhookEventActivityProvider(IWebhookSourceProvider webhookSourceProvider, IActivityDescriber activityDescriber) : IActivityProvider
|
||||
{
|
||||
public async ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sources = await webhookSourceProvider.ListAsync(cancellationToken);
|
||||
var activities = (await Task.WhenAll(sources.Select(async x => await CreateActivityDescriptors(x, cancellationToken)))).SelectMany(x => x).ToList();
|
||||
return activities;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<ActivityDescriptor>> CreateActivityDescriptors(WebhookSource webhookSource, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var descriptorTasks = webhookSource.EventTypes
|
||||
.Where(x => x.ActivityBinding != null)
|
||||
.Select(async eventType => await CreateActivityDescriptorAsync(webhookSource, eventType, cancellationToken))
|
||||
.ToList();
|
||||
|
||||
return await Task.WhenAll(descriptorTasks);
|
||||
}
|
||||
|
||||
private async Task<ActivityDescriptor> CreateActivityDescriptorAsync(WebhookSource webhookSource, WebhookSourceEventType eventType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var activityBinding = eventType.ActivityBinding!;
|
||||
var eventTypeDescription = string.IsNullOrWhiteSpace(activityBinding.Description) ? $"Handles {eventType.EventType} webhook events" : activityBinding.Description;
|
||||
var fullTypeName = activityBinding.TypeName; //webhookSource.GetWebhookActivityTypeName(eventType.EventType);
|
||||
var activityDescriptor = await activityDescriber.DescribeActivityAsync(typeof(WebhookEventReceived), cancellationToken);
|
||||
|
||||
activityDescriptor.TypeName = fullTypeName;
|
||||
activityDescriptor.Name = activityBinding.DisplayName;
|
||||
activityDescriptor.DisplayName = activityBinding.DisplayName;
|
||||
activityDescriptor.Category = webhookSource.Name;
|
||||
activityDescriptor.Description = eventTypeDescription;
|
||||
activityDescriptor.Constructor = context =>
|
||||
{
|
||||
var result = context.CreateActivity<WebhookEventReceived>();
|
||||
var activity = result.Activity;
|
||||
activity.Type = fullTypeName;
|
||||
activity.EventType = eventType.EventType;
|
||||
activity.PayloadType = eventType.PayloadType;
|
||||
return result;
|
||||
};
|
||||
|
||||
var eventTypeDescriptor = activityDescriptor.Inputs.First(x => x.Name == nameof(WebhookEventReceived.EventType));
|
||||
var eventPayloadTypeDescriptor = activityDescriptor.Inputs.First(x => x.Name == nameof(WebhookEventReceived.PayloadType));
|
||||
var payloadOutputDescriptor = activityDescriptor.Outputs.First(x => x.Name == nameof(WebhookEventReceived.Payload));
|
||||
|
||||
eventTypeDescriptor.IsBrowsable = false;
|
||||
eventPayloadTypeDescriptor.IsBrowsable = false;
|
||||
payloadOutputDescriptor.Type = eventType.PayloadType ?? typeof(object);
|
||||
|
||||
return activityDescriptor;
|
||||
}
|
||||
}
|
||||
20
src/modules/Elsa.Http.Webhooks/Elsa.Http.Webhooks.csproj
Normal file
20
src/modules/Elsa.Http.Webhooks/Elsa.Http.Webhooks.csproj
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides a way to register webhook endpoints that should be invoked when certain event occur.
|
||||
</Description>
|
||||
<PackageTags>elsa extension module, webhooks</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" />
|
||||
<PackageReference Include="WebhooksCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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/=extensions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using Elsa.Abstractions;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Http.Webhooks.Notifications;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using WebhooksCore;
|
||||
|
||||
namespace Elsa.Http.Webhooks.Endpoints.Webhooks;
|
||||
|
||||
/// <summary>
|
||||
/// An API endpoint that receives webhook events from a webhook source.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
internal class Post(IWebhookSourceProvider webhookSourceProvider, INotificationSender notificationSender, IStimulusSender stimulusSender) : ElsaEndpointWithoutRequest
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
{
|
||||
Routes("/webhooks");
|
||||
Verbs(FastEndpoints.Http.POST);
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task HandleAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var webhookEvent = (await HttpContext.Request.ReadFromJsonAsync<WebhookEvent>(cancellationToken))!;
|
||||
var webhookSources = (await webhookSourceProvider.ListAsync(cancellationToken)).ToList();
|
||||
var matchingSource = webhookSources.FirstOrDefault(source => source.EventTypes.Any(eventType => eventType.EventType == webhookEvent.EventType));
|
||||
|
||||
if (matchingSource == null)
|
||||
{
|
||||
await Send.OkAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var notification = new WebhookEventReceived(webhookEvent, matchingSource);
|
||||
await notificationSender.SendAsync(notification, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using Elsa.Features.Services;
|
||||
using Elsa.Http.Webhooks.Features;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Adds extensions to <see cref="IModule"/> that enables the <see cref="WebhooksFeature"/> feature.
|
||||
/// </summary>
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables and configures the <see cref="WebhooksFeature"/> feature.
|
||||
/// </summary>
|
||||
public static IModule UseWebhooks(this IModule module, Action<WebhooksFeature>? configure = null)
|
||||
{
|
||||
module.Configure(configure);
|
||||
return module;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
using Elsa.Extensions;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using WebhooksCore;
|
||||
using WebhooksCore.Options;
|
||||
|
||||
namespace Elsa.Http.Webhooks;
|
||||
|
||||
[UsedImplicitly]
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the specified webhook with <see cref="WebhookSinksOptions"/>
|
||||
/// </summary>
|
||||
public static IServiceCollection RegisterWebhookSink(this IServiceCollection services, Uri endpoint)
|
||||
{
|
||||
var sink = new WebhookSink
|
||||
{
|
||||
Id = endpoint.ToString(),
|
||||
Url = endpoint
|
||||
};
|
||||
return services.RegisterSink(sink);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified webhook with <see cref="WebhookSinksOptions"/>
|
||||
/// </summary>
|
||||
public static IServiceCollection RegisterSink(this IServiceCollection services, WebhookSink sink) => RegisterSinks(services, sink);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified webhooks with <see cref="WebhookSinksOptions"/>
|
||||
/// </summary>
|
||||
public static IServiceCollection RegisterSinks(this IServiceCollection services, params WebhookSink[] sinks)
|
||||
{
|
||||
services.Configure<WebhookSinksOptions>(options => options.Sinks.AddRange(sinks));
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified webhook source with <see cref="WebhookSourcesOptions"/>
|
||||
/// </summary>
|
||||
public static IServiceCollection RegisterWebhookSource(this IServiceCollection services, WebhookSource source) => services.RegisterWebhookSources(source);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified webhook sources with <see cref="WebhookSourcesOptions"/>
|
||||
/// </summary>
|
||||
public static IServiceCollection RegisterWebhookSources(this IServiceCollection services, params WebhookSource[] sources)
|
||||
{
|
||||
services.Configure<WebhookSourcesOptions>(options => options.Sources.AddRange(sources));
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using Humanizer;
|
||||
using WebhooksCore;
|
||||
|
||||
namespace Elsa.Http.Webhooks;
|
||||
|
||||
public static class WebhookActivityTypeNameHelper
|
||||
{
|
||||
public static string GetWebhookActivityTypeName(this WebhookSource webhookSource, string eventType)
|
||||
{
|
||||
var ns = GetWebhookActivityNamespace(webhookSource);
|
||||
return $"{ns}.{eventType.Dehumanize()}";
|
||||
}
|
||||
|
||||
public static string GetWebhookActivityNamespace(this WebhookSource webhookSource)
|
||||
{
|
||||
var webhookSourceName = webhookSource.Name.Dehumanize();
|
||||
return $"Webhooks.{webhookSourceName}";
|
||||
}
|
||||
}
|
||||
79
src/modules/Elsa.Http.Webhooks/Features/WebhooksFeature.cs
Normal file
79
src/modules/Elsa.Http.Webhooks/Features/WebhooksFeature.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Http.Webhooks.ActivityProviders;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using WebhooksCore;
|
||||
using WebhooksCore.Options;
|
||||
|
||||
namespace Elsa.Http.Webhooks.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Installs and configures webhook services.
|
||||
/// </summary>
|
||||
public class WebhooksFeature(IModule module) : FeatureBase(module)
|
||||
{
|
||||
public Action<WebhookSinksOptions> ConfigureSinks { get; set; } = _ => { };
|
||||
public Action<WebhookSourcesOptions> ConfigureSources { get; set; } = _ => { };
|
||||
public Action<IHttpClientBuilder> ConfigureHttpClient { get; set; } = _ => { };
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified webhook with <see cref="WebhookSinksOptions"/>
|
||||
/// </summary>
|
||||
public WebhooksFeature RegisterWebhookSink(Uri endpoint)
|
||||
{
|
||||
var sink = new WebhookSink
|
||||
{
|
||||
Id = endpoint.ToString(),
|
||||
Url = endpoint
|
||||
};
|
||||
return RegisterSink(sink);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified webhook with <see cref="WebhookSinksOptions"/>
|
||||
/// </summary>
|
||||
public WebhooksFeature RegisterSink(WebhookSink sink) => RegisterSinks(sink);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified webhooks with <see cref="WebhookSinksOptions"/>
|
||||
/// </summary>
|
||||
public WebhooksFeature RegisterSinks(params WebhookSink[] sinks)
|
||||
{
|
||||
ConfigureSinks += options => options.Sinks.AddRange(sinks);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified webhook source with <see cref="WebhookSourcesOptions"/>
|
||||
/// </summary>
|
||||
public WebhooksFeature RegisterWebhookSource(WebhookSource source) => RegisterWebhookSources(source);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified webhook sources with <see cref="WebhookSourcesOptions"/>
|
||||
/// </summary>
|
||||
public WebhooksFeature RegisterWebhookSources(params WebhookSource[] sources)
|
||||
{
|
||||
ConfigureSources += options => options.Sources.AddRange(sources);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Module
|
||||
.AddVariableTypeAndAlias<WebhookEvent>("WebhookEvent", "Webhooks")
|
||||
.AddFastEndpointsAssembly(GetType());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply()
|
||||
{
|
||||
Services.Configure(ConfigureSinks);
|
||||
Services.Configure(ConfigureSources);
|
||||
|
||||
Services
|
||||
.AddWebhooksCore(ConfigureHttpClient)
|
||||
.AddActivityProvider<WebhookEventActivityProvider>()
|
||||
.AddNotificationHandlersFrom<WebhooksFeature>();
|
||||
}
|
||||
}
|
||||
3
src/modules/Elsa.Http.Webhooks/FodyWeavers.xml
Normal file
3
src/modules/Elsa.Http.Webhooks/FodyWeavers.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Http.Webhooks.Notifications;
|
||||
using Elsa.Http.Webhooks.Stimuli;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using WebhooksCore;
|
||||
|
||||
namespace Elsa.Http.Webhooks.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes webhook activities.
|
||||
/// </summary>
|
||||
public class InvokeWebhookActivities(IStimulusSender stimulusSender) : INotificationHandler<WebhookEventReceived>
|
||||
{
|
||||
public async Task HandleAsync(WebhookEventReceived notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var matchingSource = notification.WebhookSource;
|
||||
var webhookEvent = notification.WebhookEvent;
|
||||
var fullTypeName = matchingSource.GetWebhookActivityTypeName(webhookEvent.EventType);
|
||||
var stimulus = new WebhookEventReceivedStimulus(webhookEvent.EventType);
|
||||
var input = new Dictionary<string, object>
|
||||
{
|
||||
[nameof(WebhookEvent)] = webhookEvent
|
||||
};
|
||||
var metadata = new StimulusMetadata
|
||||
{
|
||||
Input = input
|
||||
};
|
||||
await stimulusSender.SendAsync(fullTypeName, stimulus, metadata, cancellationToken);
|
||||
}
|
||||
}
|
||||
41
src/modules/Elsa.Http.Webhooks/Handlers/RunTaskHandler.cs
Normal file
41
src/modules/Elsa.Http.Webhooks/Handlers/RunTaskHandler.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Http.Webhooks.Models;
|
||||
using Elsa.Workflows.Runtime.Notifications;
|
||||
using JetBrains.Annotations;
|
||||
using WebhooksCore;
|
||||
|
||||
namespace Elsa.Http.Webhooks.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the <see cref="RunTaskRequest"/> notification and asynchronously invokes all registered webhook endpoints.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class RunTaskHandler(IWebhookEventBroadcaster webhookDispatcher) : INotificationHandler<RunTaskRequest>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task HandleAsync(RunTaskRequest notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var activityExecutionContext = notification.ActivityExecutionContext;
|
||||
var workflowExecutionContext = activityExecutionContext.WorkflowExecutionContext;
|
||||
var workflowInstanceId = workflowExecutionContext.Id;
|
||||
var correlationId = workflowExecutionContext.CorrelationId;
|
||||
var workflow = workflowExecutionContext.Workflow;
|
||||
var workflowDefinitionId = workflow.Identity.DefinitionId;
|
||||
var workflowName = workflow.WorkflowMetadata.Name;
|
||||
var tenantId = workflowExecutionContext.Workflow.Identity.TenantId;
|
||||
|
||||
var payload = new RunTaskWebhookPayload(
|
||||
workflowInstanceId,
|
||||
workflowDefinitionId,
|
||||
tenantId,
|
||||
workflowName,
|
||||
correlationId,
|
||||
notification.TaskId,
|
||||
notification.TaskName,
|
||||
notification.TaskPayload
|
||||
);
|
||||
|
||||
var webhookEvent = new NewWebhookEvent("Elsa.RunTask", payload);
|
||||
await webhookDispatcher.BroadcastAsync(webhookEvent, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
namespace Elsa.Http.Webhooks.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Stores payload information about the RunTask webhook event type.
|
||||
/// </summary>
|
||||
public record RunTaskWebhookPayload(
|
||||
string WorkflowInstanceId,
|
||||
string WorkflowDefinitionId,
|
||||
string? TenantId,
|
||||
string? WorkflowName,
|
||||
string? CorrelationId,
|
||||
string TaskId,
|
||||
string TaskName,
|
||||
object? TaskPayload);
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using WebhooksCore;
|
||||
|
||||
namespace Elsa.Http.Webhooks.Notifications;
|
||||
|
||||
public record WebhookEventReceived(WebhookEvent WebhookEvent, WebhookSource WebhookSource) : INotification;
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
using CShells.FastEndpoints.Features;
|
||||
using CShells.Features;
|
||||
using Elsa.Http.Webhooks.ActivityProviders;
|
||||
using Elsa.Workflows.Management.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using WebhooksCore;
|
||||
|
||||
namespace Elsa.Http.Webhooks.ShellFeatures;
|
||||
|
||||
/// <summary>
|
||||
/// Installs and configures webhook services.
|
||||
/// </summary>
|
||||
[ShellFeature(DisplayName = "Webhooks", Description = "Provides support for sending and receiving webhooks")]
|
||||
public class WebhooksFeature : IFastEndpointsShellFeature
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddVariableTypeAndAlias<WebhookEvent>("WebhookEvent", "Webhooks");
|
||||
|
||||
services
|
||||
.AddWebhooksCore(ConfigureHttpClient)
|
||||
.AddActivityProvider<WebhookEventActivityProvider>()
|
||||
.AddNotificationHandlersFrom<WebhooksFeature>();
|
||||
}
|
||||
|
||||
private void ConfigureHttpClient(IHttpClientBuilder httpClient)
|
||||
{
|
||||
// TODO: Configure HTTP client with configurable parameters and resilience.
|
||||
httpClient.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
|
||||
{
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
|
||||
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
|
||||
EnableMultipleHttp2Connections = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
namespace Elsa.Http.Webhooks.Stimuli;
|
||||
|
||||
public record WebhookEventReceivedStimulus(string EventType);
|
||||
|
|
@ -19,20 +19,47 @@ internal class Reload(IShellRegistry shellRegistry, IApiSerializer apiSerializer
|
|||
public override async Task HandleAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var shellId = Route<string>("shellId")!;
|
||||
var result = await shellRegistry.ReloadAsync(shellId, cancellationToken);
|
||||
ReloadResult result;
|
||||
|
||||
// CShells 0.0.15 surfaces failures via ReloadResult.Error rather than throwing — translate to 404
|
||||
// (which historically signalled "blueprint not found") for any composition / initialization error.
|
||||
if (result.Error is not null)
|
||||
try
|
||||
{
|
||||
var failedResponse = new ShellReloadResponse
|
||||
result = await shellRegistry.ReloadAsync(shellId, cancellationToken);
|
||||
}
|
||||
catch (ShellBlueprintNotFoundException ex)
|
||||
{
|
||||
var notFoundResponse = new ShellReloadResponse
|
||||
{
|
||||
Status = ShellReloadStatus.NotFound,
|
||||
RequestedShellId = shellId,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Message = ex.Message
|
||||
};
|
||||
await SendResponseAsync(notFoundResponse, StatusCodes.Status404NotFound, cancellationToken);
|
||||
return;
|
||||
}
|
||||
catch (ShellBlueprintUnavailableException ex)
|
||||
{
|
||||
var unavailableResponse = new ShellReloadResponse
|
||||
{
|
||||
Status = ShellReloadStatus.Failed,
|
||||
RequestedShellId = shellId,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Message = ex.Message
|
||||
};
|
||||
await SendResponseAsync(unavailableResponse, StatusCodes.Status503ServiceUnavailable, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.Error is not null)
|
||||
{
|
||||
var failedResponse = new ShellReloadResponse
|
||||
{
|
||||
Status = ShellReloadStatus.Failed,
|
||||
RequestedShellId = shellId,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Message = result.Error.Message
|
||||
};
|
||||
await SendResponseAsync(failedResponse, StatusCodes.Status404NotFound, cancellationToken);
|
||||
await SendResponseAsync(failedResponse, StatusCodes.Status503ServiceUnavailable, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
14
test/unit/Elsa.Common.UnitTests/CShellsCompatibilityTests.cs
Normal file
14
test/unit/Elsa.Common.UnitTests/CShellsCompatibilityTests.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using Elsa.Common.ShellFeatures;
|
||||
|
||||
namespace Elsa.Common.UnitTests;
|
||||
|
||||
public class CShellsCompatibilityTests
|
||||
{
|
||||
[Fact]
|
||||
public void ElsaCommonTypesCanBeLoadedAgainstQuartzMinimumCShellsVersion()
|
||||
{
|
||||
var exception = Record.Exception(() => typeof(MultitenancyFeature).Assembly.GetTypes());
|
||||
|
||||
Assert.Null(exception);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,10 @@
|
|||
<Threshold>0</Threshold>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CShells.Abstractions" VersionOverride="0.0.21" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Common\Elsa.Common.csproj" />
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ public class ConfiguredApplicationInstanceNameProviderTests
|
|||
Assert.False(string.IsNullOrWhiteSpace(name2));
|
||||
Assert.Matches(@"^[0-9a-f]+$", name1);
|
||||
Assert.Matches(@"^[0-9a-f]+$", name2);
|
||||
Assert.NotEqual(name1, name2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -135,9 +136,13 @@ public class ConfiguredApplicationInstanceNameProviderTests
|
|||
Environment.SetEnvironmentVariable(variable, null);
|
||||
|
||||
var name1 = CreateProvider(new() { InstanceNameEnvironmentVariable = variable }).GetName();
|
||||
var name2 = CreateProvider(new() { InstanceNameEnvironmentVariable = variable }).GetName();
|
||||
|
||||
Assert.False(string.IsNullOrWhiteSpace(name1));
|
||||
Assert.False(string.IsNullOrWhiteSpace(name2));
|
||||
Assert.Matches(@"^[0-9a-f]+$", name1);
|
||||
Assert.Matches(@"^[0-9a-f]+$", name2);
|
||||
Assert.NotEqual(name1, name2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -176,6 +181,18 @@ public class ConfiguredApplicationInstanceNameProviderTests
|
|||
Assert.Contains("contains invalid characters", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplicitInstanceName_InvalidCharactersAndTooLong_ReportsBothProblems()
|
||||
{
|
||||
var instanceName = new string('a', ConfiguredInstanceNameMaxLength) + " b";
|
||||
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => CreateProvider(new() { InstanceName = instanceName }));
|
||||
|
||||
Assert.Contains("contains invalid characters", exception.Message);
|
||||
Assert.Contains($"{ConfiguredInstanceNameMaxLength} characters or fewer", exception.Message);
|
||||
Assert.Contains("Azure Service Bus", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnvironmentVariableValue_TooLong_IsShortenedDeterministically()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,11 +24,9 @@ public class ReloadTests : ShellsApiTestBase
|
|||
[Fact]
|
||||
public async Task Post_WhenShellNotFound_Returns404WithNotFoundStatus()
|
||||
{
|
||||
// CShells 0.0.15 surfaces failures via ReloadResult.Error rather than throwing — the endpoint maps any
|
||||
// non-null Error to a 404 (the historical "blueprint not found" response).
|
||||
ShellRegistry
|
||||
.ReloadAsync(ShellId, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(new ReloadResult(ShellId, null, null, new InvalidOperationException("Shell 'test-shell' not found"))));
|
||||
.Returns(Task.FromException<ReloadResult>(new ShellBlueprintNotFoundException(ShellId)));
|
||||
|
||||
var response = await HttpClient.PostAsync($"/shells/{ShellId}/reload", null);
|
||||
|
||||
|
|
@ -37,6 +35,40 @@ public class ReloadTests : ShellsApiTestBase
|
|||
Assert.NotNull(body);
|
||||
Assert.Equal("NotFound", body.Status);
|
||||
Assert.Equal(ShellId, body.RequestedShellId);
|
||||
Assert.Equal("Shell 'test-shell' not found", body.Message);
|
||||
Assert.Contains(ShellId, body.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Post_WhenReloadFails_Returns503WithFailedStatus()
|
||||
{
|
||||
ShellRegistry
|
||||
.ReloadAsync(ShellId, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(new ReloadResult(ShellId, null, null, new InvalidOperationException("Shell reload failed"))));
|
||||
|
||||
var response = await HttpClient.PostAsync($"/shells/{ShellId}/reload", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<ShellReloadResult>(JsonOptions);
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal("Failed", body.Status);
|
||||
Assert.Equal(ShellId, body.RequestedShellId);
|
||||
Assert.Equal("Shell reload failed", body.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Post_WhenBlueprintUnavailable_Returns503WithFailedStatus()
|
||||
{
|
||||
ShellRegistry
|
||||
.ReloadAsync(ShellId, Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromException<ReloadResult>(new ShellBlueprintUnavailableException(ShellId, new InvalidOperationException())));
|
||||
|
||||
var response = await HttpClient.PostAsync($"/shells/{ShellId}/reload", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<ShellReloadResult>(JsonOptions);
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal("Failed", body.Status);
|
||||
Assert.Equal(ShellId, body.RequestedShellId);
|
||||
Assert.Contains(ShellId, body.Message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue