feat(devops): add the Elsa.DevOps.AzureDevOps module - #199
RalfvandenBurg wants to merge 1 commit into
Conversation
Integrates Azure DevOps with Elsa Workflows, following the structure and conventions of the neighbouring Elsa.DevOps.GitHub module. Activities cover repositories, pull requests, work items and builds, plus Service Hook triggers, a webhook endpoint, polling workflows for hosts that cannot receive webhooks, and display activities that suspend on a bookmark until a person confirms what they were shown. Credentials are read through IAzureDevOpsSecretReader, a thin adapter over ISecretManager. ISecretResolver would be the obvious dependency but it throws when a secret is absent, and absence is the ordinary case here: the token lookup walks several rungs and the webhook endpoint has to be able to report that no credential is configured rather than fail the request. This supersedes elsa-workflows#125, which targeted the obsolete 3.6 release train. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
PR author is not in the allowed authors list. |
|
@sfmskywalker as requested a new pr based upon main |
|
@sfmskywalker — a design question before you spend review time on this one. Roughly a third of this PR (36 files, ~3,200 of 8,800 lines) is polling: five system workflows, four dispatchers, the poll activities, and an IHostedService that starts them at host startup. None of that is the primary way to get events out of Azure DevOps — Service Hooks are, and this PR has those too. The polling exists as a workaround. Creating a Service Hook subscription needs project-level permission that a workflow author often does not have, and where that is centrally administered, waiting for a ticket is the difference between shipping and not. Polling gets the same events over the REST API with no permission beyond the PAT the activities already use. It is off by default, behind a master switch and a per-family switch, and it deliberately raises the same stimulus as the webhook path, so a trigger cannot tell which one woke it. So: should it live in its own assembly — Elsa.DevOps.AzureDevOps.Polling, say? For: A host that can use webhooks still pays for it today: a hosted service at startup, five workflow definitions appearing in the designer, and the Elsa.Scheduling reference, which nothing outside the polling workflows uses. The dispatchers share credential resolution, the event handler and the event types with the webhook path. A split turns an internal seam into a public one, and I would not want that to erode the guarantee that both paths produce an identical stimulus. |
|
Triage: needs review — community/non-draft PR on main with no review decision yet. |
Adds the
Elsa.DevOps.AzureDevOpsmodule, so workflows can drive Azure DevOps repositories, pull requests, work items and builds, and can be driven by them. It follows the structure and conventions of the neighbouringElsa.DevOps.GitHubmodule.Supersedes #125, which was closed because it targeted the obsolete 3.6 release train. This is a fresh branch off
main, not a rebase: the retarget turned out to be more than a replay, and the module has grown since. Closes #124.Tasks
maininstead ofrelease/3.6.0net8.0;net9.0;net10.0Elsa.Extensions.sln,Directory.Packages.propsand the README tableWhat it contains
GetRepository,GetBranch,ListBranches,DisplayRepositoryCreatePullRequest,GetPullRequest,ListPullRequests,WatchAzureDevOpsPullRequest,DisplayPullRequestCreateWorkItem,GetWorkItem,UpdateWorkItem,QueryWorkItems,AddWorkItemComment,AddWorkItemHyperlink,AddWorkItemRelation,DisplayWorkItemGetBuild,ListBuilds,QueueBuild,DisplayBuildBesides the activities:
webhooks/azure-devopsbehind a shared-secret basic auth check.UserTokenSecretNameFormat), so work it creates is attributed to that person rather than to a shared service identity. Off unless configured.Review comments from #125
All three were fixed before this branch was cut, and the fixes are in this diff:
AzureDevOpsConnectionFactorynow keys the cache onorganizationUrl|SHA256(token).VssConnectioncached indefinitely without disposal. The factory implementsIDisposableand disposes every cached connection.UpdateWorkItemcreating connections twice. It now takes the connection once and reuses it.One thing worth flagging
ISecretResolverlooked like the right dependency for reading credentials, but it throws when a secret is absent, and absence is the ordinary case here: the token lookup walks several rungs (the secret named after the calling user, then the configured default) and has to pass over an empty one, and the webhook endpoint has to be able to answer "no credential is configured" rather than fail the request with a 500.So the module defines
IAzureDevOpsSecretReader, a thin adapter overISecretManagerthat returnsnullfor a name it does not know. It is registered withTryAdd, so a host that reads credentials from somewhere else can supply its own. If you would rather see a nullable overload onISecretResolverin elsa-core, I am happy to drop the adapter and follow that instead.How to verify
295 unit tests, all passing; the module builds for all three target frameworks. The tests are offline — nothing reaches Azure DevOps.
On the size of this PR
CONTRIBUTING asks for one concern per PR, and this is larger than that ideal. It is one module arriving whole rather than several concerns mixed together, but if you would rather review it in pieces, the natural split is:
Say the word and I will split it that way.
🤖 Generated with Claude Code