Honor publish result in Publish/BulkPublish endpoints

The Publish and BulkPublish API endpoints invoked PublishAsync but ignored
the returned result.Succeeded, so publications that failed due to validation
errors were silently reported as successful. On main, strict publishing is the
default, making this a high-impact correctness issue.

- Publish endpoint: when the publish result indicates failure, surface the
  validation errors via AddError and return a 400, mirroring the existing
  save-and-publish (Post) endpoint pattern.
- BulkPublish endpoint: add a Failed bucket; definitions whose publication
  fails are recorded in Failed instead of Published, and the new bucket is
  returned on the response.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Sipke Schoorstra 2026-06-19 21:13:01 +02:00
parent b266b32ef9
commit 354d59942c
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
3 changed files with 21 additions and 2 deletions

View file

@ -41,6 +41,7 @@ internal class BulkPublish(
var alreadyPublished = new List<string>();
var skipped = new List<string>();
var updatedConsumers = new List<string>();
var failed = new List<string>();
var publishableDefinitions = new List<(string DefinitionId, WorkflowDefinition Definition)>();
var definitions = (await store.FindManyAsync(new WorkflowDefinitionFilter
@ -88,13 +89,20 @@ internal class BulkPublish(
foreach (var (definitionId, definition) in publishableDefinitions)
{
var result = await workflowDefinitionPublisher.PublishAsync(definition, cancellationToken);
if (!result.Succeeded)
{
failed.Add(definitionId);
continue;
}
published.Add(definitionId);
if (result.AffectedWorkflows.WorkflowDefinitions.Count > 0)
updatedConsumers.AddRange(result.AffectedWorkflows.WorkflowDefinitions.Select(x => x.DefinitionId));
}
return new(published, alreadyPublished, notFound, skipped, updatedConsumers);
return new(published, alreadyPublished, notFound, skipped, updatedConsumers, failed);
}
}

View file

@ -5,11 +5,12 @@ internal class Request
public ICollection<string> DefinitionIds { get; set; } = default!;
}
internal class Response(ICollection<string> published, ICollection<string> alreadyPublished, ICollection<string> notFound, ICollection<string> skipped, ICollection<string> updatedConsumers)
internal class Response(ICollection<string> published, ICollection<string> alreadyPublished, ICollection<string> notFound, ICollection<string> skipped, ICollection<string> updatedConsumers, ICollection<string> failed)
{
public ICollection<string> Published { get; } = published;
public ICollection<string> AlreadyPublished { get; } = alreadyPublished;
public ICollection<string> NotFound { get; } = notFound;
public ICollection<string> Skipped { get; } = skipped;
public ICollection<string> UpdatedConsumers { get; } = updatedConsumers;
public ICollection<string> Failed { get; } = failed;
}

View file

@ -60,6 +60,16 @@ internal class Publish(
var isPublished = definition.IsPublished;
var result = !isPublished ? await workflowDefinitionPublisher.PublishAsync(definition, cancellationToken) : null;
if (result is { Succeeded: false })
{
foreach (var validationError in result.ValidationErrors)
AddError(validationError.Message);
await Send.ErrorsAsync(400, cancellationToken);
return;
}
var mappedDefinition = await linker.MapAsync(definition, cancellationToken);
var response = new Response(mappedDefinition, isPublished, result?.AffectedWorkflows.WorkflowDefinitions.Count ?? 0);
await Send.OkAsync(response, cancellationToken);