fix compilation

This commit is contained in:
Vitali sharp8n 2026-09-03 14:34:50 +03:00
parent 3b50333353
commit 1f34453886
4 changed files with 82 additions and 55 deletions

View file

@ -332,6 +332,8 @@ public class WorkflowsController : ControllerBase
[HttpPost("{id:guid}/run")] [HttpPost("{id:guid}/run")]
[RequireScope("run")] [RequireScope("run")]
public async Task<IActionResult> Run(Guid id, [FromBody] RunRequest? request, CancellationToken ct) public async Task<IActionResult> Run(Guid id, [FromBody] RunRequest? request, CancellationToken ct)
{
try
{ {
var workflow = await _db.Workflows var workflow = await _db.Workflows
.FirstOrDefaultAsync(w => .FirstOrDefaultAsync(w =>
@ -346,6 +348,12 @@ public class WorkflowsController : ControllerBase
return Accepted(new { runId }); return Accepted(new { runId });
} }
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start workflow run {WorkflowId} for tenant {TenantId}", id, TenantId);
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
}
}
/// <summary> /// <summary>
/// Enables or disables the workflow's auto-trigger (cron/interval/webhook/ /// Enables or disables the workflow's auto-trigger (cron/interval/webhook/
@ -390,6 +398,8 @@ public class WorkflowsController : ControllerBase
[HttpPost("{id:guid}/instances/{instanceId}/resume")] [HttpPost("{id:guid}/instances/{instanceId}/resume")]
[RequireScope("run")] [RequireScope("run")]
public async Task<IActionResult> Resume(Guid id, string instanceId, [FromBody] ResumeRequest? request, CancellationToken ct) public async Task<IActionResult> Resume(Guid id, string instanceId, [FromBody] ResumeRequest? request, CancellationToken ct)
{
try
{ {
var workflow = await _db.Workflows var workflow = await _db.Workflows
.Include(w => w.Tasks) .Include(w => w.Tasks)
@ -412,4 +422,10 @@ public class WorkflowsController : ControllerBase
return Accepted(new { runId, resumedTaskId = checkpoint.TaskId }); return Accepted(new { runId, resumedTaskId = checkpoint.TaskId });
} }
catch (Exception ex)
{
_logger.LogError(ex, "Failed to resume workflow {WorkflowId} instance {InstanceId} for tenant {TenantId}", id, instanceId, TenantId);
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
}
}
} }

View file

@ -223,6 +223,22 @@ builder.Services.AddCors(options =>
var app = builder.Build(); var app = builder.Build();
app.UseCors(); app.UseCors();
// Global exception handler: catches unhandled exceptions from any controller
// or middleware and returns a structured JSON error instead of a raw 500.
app.UseExceptionHandler(handler =>
{
handler.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/json";
var ex = context.Features.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>();
var message = ex?.Error.Message ?? "An unexpected error occurred.";
var json = System.Text.Json.JsonSerializer.Serialize(new { error = message });
await context.Response.WriteAsync(json);
});
});
app.UseMiddleware<AuthMiddleware>(); app.UseMiddleware<AuthMiddleware>();
// Resolve the Forgejo-backed workflow source asynchronously after auth. // Resolve the Forgejo-backed workflow source asynchronously after auth.

View file

@ -9,10 +9,14 @@ using Microsoft.CodeAnalysis.CSharp;
namespace w4c_workflows.Services.Execution; namespace w4c_workflows.Services.Execution;
/// <summary> /// <summary>
/// Executes multi-file C# with the Roslyn compiler: every <c>.cs</c> file under /// Executes single-file C# with the Roslyn compiler: the entry <c>.cs</c> file
/// the task's working directory is compiled together into an in-memory assembly /// specified by the task's <c>entry.file</c> is compiled into an in-memory
/// (one syntax tree per file, so per-file <c>using</c> directives and file-scoped /// assembly, then the entry method is invoked reflectively.
/// namespaces work), then the entry method is invoked reflectively. ///
/// Only the entry file is compiled — not every <c>.cs</c> file under the
/// working directory. Compiling all files would merge unrelated task sources
/// (e.g. <c>Main.cs</c> + <c>tasks/hello-world.cs</c>) into one assembly,
/// causing duplicate-type errors when both define a <c>Program</c> class.
/// ///
/// Each execution uses a <see cref="CollectibleAssemblyLoadContext"/> so the /// Each execution uses a <see cref="CollectibleAssemblyLoadContext"/> so the
/// loaded assembly and all its types can be garbage-collected after the call /// loaded assembly and all its types can be garbage-collected after the call
@ -44,12 +48,14 @@ public class CSharpScriptExecutor : IScriptExecutor
try try
{ {
var workingDir = ExecutionHelpers.ResolveWorkingDir(invocation); var workingDir = ExecutionHelpers.ResolveWorkingDir(invocation);
var sources = EnumerateSources(workingDir); var entryPath = Path.GetFullPath(Path.Combine(workingDir, invocation.EntryFile));
if (sources.Count == 0) if (!File.Exists(entryPath))
return Task.FromResult(Fail("no .cs source files found under the task directory", sw.Elapsed)); return Task.FromResult(Fail($"entry file not found: {invocation.EntryFile}", sw.Elapsed));
var source = new SourceFile(invocation.EntryFile, File.ReadAllText(entryPath));
// Compilation is CPU-bound and synchronous; keep it off the request thread. // Compilation is CPU-bound and synchronous; keep it off the request thread.
var (assemblyLoadContext, assembly, diagnostics) = Compile(sources); var (assemblyLoadContext, assembly, diagnostics) = Compile(source);
if (assembly == null) if (assembly == null)
{ {
assemblyLoadContext?.Unload(); assemblyLoadContext?.Unload();
@ -84,27 +90,13 @@ public class CSharpScriptExecutor : IScriptExecutor
private static ExecutionResult Fail(string message, TimeSpan duration) private static ExecutionResult Fail(string message, TimeSpan duration)
=> new(false, -1, string.Empty, string.Empty, null, message, duration); => new(false, -1, string.Empty, string.Empty, null, message, duration);
private static IReadOnlyList<SourceFile> EnumerateSources(string workingDir) private static (AssemblyLoadContext, Assembly?, ImmutableArray<Diagnostic>) Compile(SourceFile source)
{ {
// One syntax tree per file; compilation is order-independent, so a var syntaxTree = CSharpSyntaxTree.ParseText(source.Source, path: source.Path);
// deterministic sort is all that matters for reproducibility.
return Directory.EnumerateFiles(workingDir, "*.cs", SearchOption.AllDirectories)
.Select(full => new SourceFile(
Path.GetRelativePath(workingDir, full),
File.ReadAllText(full)))
.OrderBy(s => s.Path, StringComparer.Ordinal)
.ToList();
}
private static (AssemblyLoadContext, Assembly?, ImmutableArray<Diagnostic>) Compile(IReadOnlyList<SourceFile> sources)
{
var syntaxTrees = sources
.Select(s => CSharpSyntaxTree.ParseText(s.Source, path: s.Path))
.ToArray();
var compilation = CSharpCompilation.Create( var compilation = CSharpCompilation.Create(
$"wf-cs-{Guid.NewGuid():N}", $"wf-cs-{Guid.NewGuid():N}",
syntaxTrees, [syntaxTree],
GetCachedReferences(), GetCachedReferences(),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true)); new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true));

View file

@ -35,8 +35,12 @@ public class CSharpScriptExecutorTests
} }
[Fact] [Fact]
public async Task Executes_multi_file_program() public async Task Compiles_only_entry_file_not_other_cs_files()
{ {
// Only the entry file (Program.cs) is compiled. Helper.cs exists in the
// directory but is NOT included, so referencing Helper.Add causes a
// compilation error. This prevents duplicate-type errors when multiple
// tasks each define their own Program class in the same workflow directory.
using var dir = new TempDir(); using var dir = new TempDir();
dir.Write("Helper.cs", """ dir.Write("Helper.cs", """
public static class Helper public static class Helper
@ -61,9 +65,8 @@ public class CSharpScriptExecutorTests
var result = await _executor.ExecuteAsync( var result = await _executor.ExecuteAsync(
Invocation.For("csharp", "Program.cs", "{\"a\":2,\"b\":3}", dir.Path), default); Invocation.For("csharp", "Program.cs", "{\"a\":2,\"b\":3}", dir.Path), default);
Assert.True(result.Success, result.Error); Assert.False(result.Success);
using var output = JsonDocument.Parse(result.Output!); Assert.Contains("compilation failed", result.Error, StringComparison.OrdinalIgnoreCase);
Assert.Equal(5, output.RootElement.GetProperty("sum").GetInt32());
} }
[Fact] [Fact]