fix(scriptanalysis): warn on scripts blocking on a Task (.Result/.Wait()/GetAwaiter().GetResult()) (review 2026-10-21 01/SR-U2)

Advisory, not a trust-policy violation: every policy entry blocks deploy, and
an error here would fail the next redeploy of already-deployed scripts.
ValidationService surfaces it as a non-blocking ScriptCompilation warning.
This commit is contained in:
Joseph Doherty
2026-10-22 06:22:01 -04:00
parent 2ca60f86ff
commit 5919a71b8b
8 changed files with 270 additions and 17 deletions
@@ -99,4 +99,40 @@ public class ScriptCompiler
? Result<bool>.Success(true)
: Result<bool>.Failure($"Script '{scriptName}' {error}");
}
/// <summary>
/// Non-blocking advisories for a script that already compiles — today, synchronous
/// blocking on a <see cref="System.Threading.Tasks.Task"/> (<c>.Result</c>,
/// <c>.Wait()</c>, <c>.GetAwaiter().GetResult()</c>), which can starve the site's
/// script-execution pool (review 2026-10-21 01/SR-U2). Memoised process-wide like
/// the compile verdict, under its own surface key. A faulting analysis yields no
/// advisories (and is not cached) — an advisory must never fail validation.
/// </summary>
/// <param name="code">The C# script code.</param>
/// <returns>Advisory messages; empty when there is nothing to flag.</returns>
public IReadOnlyList<string> FindAdvisories(string code)
{
if (string.IsNullOrWhiteSpace(code))
return [];
try
{
var (_, joined) = ScriptCompileVerdictCache.GetOrAdd(AdvisorySurface, code, () =>
{
var advisories = BlockingWaitAnalyzer.Find(code, typeof(ScriptCompileSurface));
return (true, advisories.Count == 0 ? null : string.Join(AdvisorySeparator, advisories));
});
return joined == null ? [] : joined.Split(AdvisorySeparator);
}
catch (Exception)
{
return [];
}
}
/// <summary>Cache surface key for the advisory pass (distinct from the compile verdict).</summary>
private const string AdvisorySurface = nameof(ScriptCompileSurface) + "/advisories";
/// <summary>Separator for the advisories packed into one cached string.</summary>
private const string AdvisorySeparator = "\n";
}
@@ -269,7 +269,8 @@ public class ValidationService
}
/// <summary>
/// Validates that all scripts compile successfully using the ScriptCompiler.
/// Validates that all scripts compile successfully using the ScriptCompiler, and adds
/// non-blocking warnings for compiled scripts that block synchronously on a Task.
/// </summary>
/// <param name="configuration">The flattened configuration to validate.</param>
/// <returns>A <see cref="ValidationResult"/> with errors for each script that fails compilation.</returns>
@@ -286,6 +287,16 @@ public class ValidationService
errors.Add(ValidationEntry.Error(ValidationCategory.ScriptCompilation,
$"Script '{script.CanonicalName}' failed compilation: {result.Error}",
script.CanonicalName));
continue;
}
// Advisories never gate deploy (review 2026-10-21 01/SR-U2): a script that
// blocks on a Task still deploys, but the operator sees why it is risky.
foreach (var advisory in _scriptCompiler.FindAdvisories(script.Code))
{
warnings.Add(ValidationEntry.Warning(ValidationCategory.ScriptCompilation,
$"Script '{script.CanonicalName}' {advisory}",
script.CanonicalName));
}
}