Skip to content

Commit b3ae8cb

Browse files
authored
fix: apply prerelease-aware range check when resolving installed workers (#5287)
* fix: apply prerelease-aware range check when resolving installed workers Resolves #5286 The built-in profile (e.g. flex) pins workers to exact stable ranges like node [3.13.0], but the v5 preview catalog only ships 3.13.0-preview.1. Per NuGet, 3.13.0-preview.1 < 3.13.0, so VersionRange.Satisfies rejects the installed prerelease worker and 'func start' fails at the resolve_worker step. PR #5257 fixed the same NuGet quirk on the catalog search path (WorkloadCatalog.SatisfiesRange) but missed the installed-worker resolver. Extract the prerelease-aware check into a shared helper (WorkloadVersionRanges.SatisfiesRange) and use it from DefaultFunctionsWorkerContentResolver as well, driven by the same WorkloadCatalogOptions.IncludePrerelease that auto-enables on prerelease CLI builds. * release: bump to 5.0.0-preview.2 and note worker resolver fix
1 parent bf3df06 commit b3ae8cb

7 files changed

Lines changed: 116 additions & 30 deletions

File tree

src/Directory.Version.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<VersionPrefix>5.0.0</VersionPrefix>
5-
<VersionSuffix>preview.1</VersionSuffix>
5+
<VersionSuffix>preview.2</VersionSuffix>
66
<UpdateBuildNumber>true</UpdateBuildNumber>
77
</PropertyGroup>
88

src/Func/Workers/DefaultFunctionsWorkerContentResolver.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,27 @@
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

44
using Azure.Functions.Cli.Workloads;
5+
using Azure.Functions.Cli.Workloads.Catalog;
6+
using Microsoft.Extensions.Options;
57
using NuGet.Versioning;
68

79
namespace Azure.Functions.Cli.Workers;
810

911
/// <summary>
1012
/// Default worker resolver for content workload payloads.
1113
/// </summary>
12-
internal sealed class DefaultFunctionsWorkerContentResolver(IWorkerConfigFileSystem workerConfigFileSystem) : IFunctionsWorkerContentResolver
14+
internal sealed class DefaultFunctionsWorkerContentResolver(
15+
IWorkerConfigFileSystem workerConfigFileSystem,
16+
IOptions<WorkloadCatalogOptions> catalogOptions) : IFunctionsWorkerContentResolver
1317
{
1418
private const string WorkerConfigFileName = "worker.config.json";
1519

1620
private readonly IWorkerConfigFileSystem _workerConfigFileSystem = workerConfigFileSystem
1721
?? throw new ArgumentNullException(nameof(workerConfigFileSystem));
1822

23+
private readonly WorkloadCatalogOptions _catalogOptions = catalogOptions?.Value
24+
?? throw new ArgumentNullException(nameof(catalogOptions));
25+
1926
public FunctionsWorkerResolutionResult ResolveWorker(
2027
FunctionsWorkerId workerId,
2128
IReadOnlyList<ContentWorkloadInfo> installedWorkers,
@@ -69,8 +76,9 @@ public FunctionsWorkerResolutionResult ResolveWorker(
6976
return FunctionsWorkerResolutionResults.Resolved(resolvedWorker);
7077
}
7178

72-
private static bool SatisfiesConstraint(NuGetVersion version, VersionRange? constraint)
73-
=> constraint is null || constraint.Satisfies(version);
79+
private bool SatisfiesConstraint(NuGetVersion version, VersionRange? constraint)
80+
=> constraint is null
81+
|| WorkloadVersionRanges.SatisfiesRange(constraint, version, _catalogOptions.IncludePrerelease);
7482

7583
private static FunctionsWorkerResolutionResult NotInstalled(FunctionsWorkerId workerId)
7684
=> NotInstalledResult(

src/Func/Workloads/Catalog/WorkloadCatalog.cs

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -109,25 +109,6 @@ private NuGetProtocolSourceClient ResolveClient(string? source)
109109
return best;
110110
}
111111

112-
// NuGet's VersionRange.Satisfies rejects prerelease candidates when the
113-
// range itself has no prerelease in its bounds (e.g. a range pinned to
114-
// "[3.13.0]" rejects "3.13.0-preview.1" even though both share the same
115-
// numeric version). When prerelease is enabled, re-check the candidate's
116-
// numeric portion against the range so prerelease versions whose numeric
117-
// version is inside the bounds are accepted.
118112
private static bool SatisfiesRange(VersionRange range, NuGetVersion candidate, bool includePrerelease)
119-
{
120-
if (range.Satisfies(candidate))
121-
{
122-
return true;
123-
}
124-
125-
if (!includePrerelease || !candidate.IsPrerelease)
126-
{
127-
return false;
128-
}
129-
130-
var numeric = new NuGetVersion(candidate.Major, candidate.Minor, candidate.Patch, candidate.Revision);
131-
return range.Satisfies(numeric);
132-
}
113+
=> WorkloadVersionRanges.SatisfiesRange(range, candidate, includePrerelease);
133114
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using NuGet.Versioning;
5+
6+
namespace Azure.Functions.Cli.Workloads.Catalog;
7+
8+
/// <summary>
9+
/// Helpers for evaluating <see cref="VersionRange"/> against <see cref="NuGetVersion"/>
10+
/// candidates in a way that accounts for NuGet's prerelease handling.
11+
/// </summary>
12+
internal static class WorkloadVersionRanges
13+
{
14+
/// <summary>
15+
/// Returns whether <paramref name="candidate"/> satisfies <paramref name="range"/>,
16+
/// honoring <paramref name="includePrerelease"/>.
17+
/// </summary>
18+
/// <remarks>
19+
/// NuGet's <see cref="VersionRange.Satisfies(NuGetVersion)"/> rejects prerelease
20+
/// candidates when the range itself has no prerelease in its bounds (for example,
21+
/// a range pinned to <c>[3.13.0]</c> rejects <c>3.13.0-preview.1</c> even though
22+
/// both share the same numeric version). When prerelease is enabled, re-check the
23+
/// candidate's numeric portion against the range so prerelease versions whose
24+
/// numeric version is inside the bounds are accepted.
25+
/// </remarks>
26+
public static bool SatisfiesRange(VersionRange range, NuGetVersion candidate, bool includePrerelease)
27+
{
28+
ArgumentNullException.ThrowIfNull(range);
29+
ArgumentNullException.ThrowIfNull(candidate);
30+
31+
if (range.Satisfies(candidate))
32+
{
33+
return true;
34+
}
35+
36+
if (!includePrerelease || !candidate.IsPrerelease)
37+
{
38+
return false;
39+
}
40+
41+
var numeric = new NuGetVersion(candidate.Major, candidate.Minor, candidate.Patch, candidate.Revision);
42+
return range.Satisfies(numeric);
43+
}
44+
}

src/Func/release_notes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22

33
#### Changes
44

5+
- Fix `func start` failing to resolve installed prerelease worker workloads against built-in profile ranges (e.g. `node [3.13.0]` now accepts `3.13.0-preview.1`). (#5286)
6+

test/Func.Tests/Workers/DefaultFunctionsWorkerInstallerTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Azure.Functions.Cli.Workloads.Discovery;
88
using Azure.Functions.Cli.Workloads.Install;
99
using Azure.Functions.Cli.Workloads.Storage;
10+
using Microsoft.Extensions.Options;
1011
using NSubstitute;
1112
using NuGet.Configuration;
1213
using NuGet.Versioning;
@@ -181,7 +182,7 @@ await Assert.ThrowsAsync<ArgumentNullException>(
181182
[Fact]
182183
public void Ctor_NullDependencies_Throw()
183184
{
184-
var contentResolver = new DefaultFunctionsWorkerContentResolver(_fileSystem);
185+
var contentResolver = new DefaultFunctionsWorkerContentResolver(_fileSystem, Options.Create(new WorkloadCatalogOptions()));
185186

186187
Assert.Throws<ArgumentNullException>(() => new DefaultFunctionsWorkerInstaller(null!, _workloadInstaller, _workloadPaths, contentResolver));
187188
Assert.Throws<ArgumentNullException>(() => new DefaultFunctionsWorkerInstaller(_workloadCatalog, null!, _workloadPaths, contentResolver));
@@ -191,7 +192,7 @@ public void Ctor_NullDependencies_Throw()
191192

192193
private DefaultFunctionsWorkerInstaller CreateInstaller()
193194
{
194-
var contentResolver = new DefaultFunctionsWorkerContentResolver(_fileSystem);
195+
var contentResolver = new DefaultFunctionsWorkerContentResolver(_fileSystem, Options.Create(new WorkloadCatalogOptions()));
195196
return new DefaultFunctionsWorkerInstaller(_workloadCatalog, _workloadInstaller, _workloadPaths, contentResolver);
196197
}
197198

test/Func.Tests/Workers/DefaultFunctionsWorkerResolverTests.cs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using Azure.Functions.Cli.Workers;
55
using Azure.Functions.Cli.Workloads;
6+
using Azure.Functions.Cli.Workloads.Catalog;
7+
using Microsoft.Extensions.Options;
68
using NSubstitute;
79
using NuGet.Versioning;
810
using Xunit;
@@ -264,7 +266,52 @@ public void Ctor_NullContentResolver_Throws()
264266
[Fact]
265267
public void ContentResolverCtor_NullWorkerConfigFileSystem_Throws()
266268
{
267-
Assert.Throws<ArgumentNullException>(() => new DefaultFunctionsWorkerContentResolver(null!));
269+
Assert.Throws<ArgumentNullException>(
270+
() => new DefaultFunctionsWorkerContentResolver(null!, Options.Create(new WorkloadCatalogOptions())));
271+
}
272+
273+
[Fact]
274+
public void ContentResolverCtor_NullCatalogOptions_Throws()
275+
{
276+
Assert.Throws<ArgumentNullException>(() => new DefaultFunctionsWorkerContentResolver(_fileSystem, null!));
277+
}
278+
279+
[Fact]
280+
public async Task ResolveWorkerAsync_ExactStableRange_ResolvesPrereleaseInstalled_WhenIncludePrereleaseTrue()
281+
{
282+
// Repro for issue #5286: built-in profile pins worker to exact stable [3.13.0]
283+
// but the catalog/installer only has 3.13.0-preview.1 installed. The resolver
284+
// must accept the prerelease when prerelease is enabled.
285+
ContentWorkloadInfo preview = CreateContentWorkload(NodeWorkerPackageId, "3.13.0-preview.1");
286+
UseContentWorkloads(preview);
287+
288+
DefaultFunctionsWorkerResolver resolver = CreateResolver(
289+
new Dictionary<string, VersionRange> { ["node"] = VersionRange.Parse("[3.13.0]") },
290+
includePrerelease: true);
291+
292+
FunctionsWorkerResolutionResult result = await resolver.ResolveWorkerAsync(
293+
new FunctionsWorkerId("node"),
294+
CancellationToken.None);
295+
296+
FunctionsWorkerResolutionResult.Resolved resolved = Assert.IsType<FunctionsWorkerResolutionResult.Resolved>(result);
297+
Assert.Equal("3.13.0-preview.1", resolved.Worker.Version);
298+
}
299+
300+
[Fact]
301+
public async Task ResolveWorkerAsync_ExactStableRange_RejectsPrereleaseInstalled_WhenIncludePrereleaseFalse()
302+
{
303+
ContentWorkloadInfo preview = CreateContentWorkload(NodeWorkerPackageId, "3.13.0-preview.1");
304+
UseContentWorkloads(preview);
305+
306+
DefaultFunctionsWorkerResolver resolver = CreateResolver(
307+
new Dictionary<string, VersionRange> { ["node"] = VersionRange.Parse("[3.13.0]") },
308+
includePrerelease: false);
309+
310+
FunctionsWorkerResolutionResult result = await resolver.ResolveWorkerAsync(
311+
new FunctionsWorkerId("node"),
312+
CancellationToken.None);
313+
314+
Assert.IsType<FunctionsWorkerResolutionResult.NotResolved>(result);
268315
}
269316

270317
private void UseContentWorkloads(params ContentWorkloadInfo[] workloads)
@@ -280,10 +327,13 @@ private void UseContentWorkloads(params ContentWorkloadInfo[] workloads)
280327
});
281328
}
282329

283-
private DefaultFunctionsWorkerResolver CreateResolver(IReadOnlyDictionary<string, VersionRange>? workerVersionRanges = null)
284-
=> new(_workloads, CreateContentResolver(), workerVersionRanges);
330+
private DefaultFunctionsWorkerResolver CreateResolver(
331+
IReadOnlyDictionary<string, VersionRange>? workerVersionRanges = null,
332+
bool includePrerelease = false)
333+
=> new(_workloads, CreateContentResolver(includePrerelease), workerVersionRanges);
285334

286-
private DefaultFunctionsWorkerContentResolver CreateContentResolver() => new(_fileSystem);
335+
private DefaultFunctionsWorkerContentResolver CreateContentResolver(bool includePrerelease = false)
336+
=> new(_fileSystem, Options.Create(new WorkloadCatalogOptions { IncludePrerelease = includePrerelease }));
287337

288338
private static ContentWorkloadInfo CreateContentWorkload(string packageId, string packageVersion, IReadOnlyList<string>? aliases = null)
289339
{

0 commit comments

Comments
 (0)