Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion conventions/update-nuget-packages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ Rule properties:
- `version`: Optional string, default `update-major`. Valid policy values are `update-major`, `update-minor`, `update-patch`, and `no-update`. A specific version such as `7.0.0` updates only to that exact version. A NuGet version range such as `[7.0.0, 8.0.0)` updates only to versions inside that range.
- `include-prerelease`: Optional boolean, default `false`. When true, prerelease candidates are eligible.
- `prerelease-channel`: Optional string. When set, candidates must be prerelease versions that use the specified prerelease label. This setting takes precedence over `include-prerelease`.
- `publish-cooldown`: Optional string, default `weekly`. `weekly` only permits versions published on or before the Tuesday before the last Tuesday. `none` permits the newest eligible version regardless of publish date. Use this for package ID patterns owned and validated by your organization.

## Behavior

The convention requires the target directory to be inside a git worktree and only edits files reported by `git ls-files`. It scans git-tracked `*.csproj`, `*.props`, `*.targets`, and any `dotnet-tools.json` file.

Package metadata is resolved from enabled NuGet package sources configured for the repository, including sources inherited from `nuget.config`. Non-HTTP package sources such as local folders are supported; because those sources use local file timestamps rather than package publish timestamps, their versions are treated as old enough for the publish-date cutoff.

Only versions published on or before the Tuesday before the last Tuesday are eligible. This gives newly published packages at least one full week before the convention can select them.
By default, only versions published on or before the Tuesday before the last Tuesday are eligible. This gives newly published packages at least one full week before the convention can select them. A matching rule can set `publish-cooldown: none` for some packages while leaving the default cooldown in place for others, such as third-party packages.

The convention leaves package reference wildcard versions, package reference version ranges, computed MSBuild expressions, and unsupported XML shapes unchanged. Same-file property expressions such as `Version="$(PackageVersion)"` are updated when the property has a single literal definition in the same file.

Expand All @@ -42,4 +43,8 @@ conventions:
version: '[8.0.0, 9.0.0)'
- packages: StackExchange.Redis
prerelease-channel: faithlife
- packages:
- Faithlife.*
- Logos.*
publish-cooldown: none
```
38 changes: 38 additions & 0 deletions conventions/update-nuget-packages/convention.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,44 @@ Describe 'update-nuget-packages convention' {
}
}

It 'allows matching package rules to skip the publish cooldown' {
$testDirectory = New-TemporaryDirectory

try {
Initialize-TestRepository -Path $testDirectory
$projectPath = Join-Path $testDirectory 'App.csproj'
WriteTestFile -Path $projectPath -Content @'
<Project>
<ItemGroup>
<PackageReference Include="Faithlife.Package" Version="1.0.0" />
<PackageReference Include="ThirdParty.Package" Version="1.0.0" />
</ItemGroup>
</Project>
'@

AddAndCommitAll -TestDirectory $testDirectory -Message 'Add project file'
$metadataPath = WriteMetadataFile -Packages @{
'Faithlife.Package' = @(@{ version = '2.0.0'; publishedUtc = '2026-05-20T00:00:00Z'; listed = $true })
'ThirdParty.Package' = @(@{ version = '2.0.0'; publishedUtc = '2026-05-20T00:00:00Z'; listed = $true })
}

InvokeUpdateNugetPackagesConvention -TestDirectory $testDirectory -Settings @{
'test-package-metadata-file' = $metadataPath
'now-utc' = '2026-05-27T12:00:00Z'
rules = @(
@{ packages = 'Faithlife.*'; 'publish-cooldown' = 'none' }
)
} | Out-Null

$content = Get-Content -LiteralPath $projectPath -Raw
$content | Should -Match 'Include="Faithlife\.Package" Version="2\.0\.0"'
$content | Should -Match 'Include="ThirdParty\.Package" Version="1\.0\.0"'
}
finally {
Remove-Item -LiteralPath $testDirectory -Recurse -Force
}
}

It 'updates same-file properties and MSBuild SDK references' {
$testDirectory = New-TemporaryDirectory

Expand Down
32 changes: 29 additions & 3 deletions conventions/update-nuget-packages/convention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ public EffectiveRule GetEffectiveRule(string packageId)

if (rule.PrereleaseChannel is not null)
effectiveRule.PrereleaseChannel = rule.PrereleaseChannel;

if (rule.PublishCooldownPolicy.HasValue)
effectiveRule.PublishCooldownPolicy = rule.PublishCooldownPolicy.Value;
}

return effectiveRule;
Expand All @@ -216,6 +219,7 @@ internal sealed class Rule
public VersionRange? AllowedVersionRange { get; }
public bool? IncludePrerelease { get; }
public string? PrereleaseChannel { get; }
public PublishCooldown? PublishCooldownPolicy { get; }

public static Rule Parse(JsonElement element)
{
Expand Down Expand Up @@ -286,7 +290,21 @@ public static Rule Parse(JsonElement element)
prereleaseChannel = prereleaseChannelElement.GetString();
}

return new Rule(packages, hasVersion, versionPolicy, exactVersion, allowedVersionRange, includePrerelease, prereleaseChannel);
PublishCooldown? publishCooldown = null;
if (element.TryGetProperty("publish-cooldown", out var publishCooldownElement))
{
if (publishCooldownElement.ValueKind != JsonValueKind.String)
throw new InvalidOperationException("Rule 'publish-cooldown' must be a string.");

publishCooldown = publishCooldownElement.GetString() switch
{
"weekly" => PublishCooldown.Weekly,
"none" => PublishCooldown.None,
_ => throw new InvalidOperationException("Rule 'publish-cooldown' must be 'weekly' or 'none'.")
};
}

return new Rule(packages, hasVersion, versionPolicy, exactVersion, allowedVersionRange, includePrerelease, prereleaseChannel, publishCooldown);
}

public bool IsMatch(string packageId) => m_packagePatterns.Any(pattern => pattern.IsMatch(packageId));
Expand Down Expand Up @@ -319,7 +337,7 @@ private static Regex CreateWildcardRegex(string pattern)
return new Regex(regex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}

private Rule(List<string> packages, bool hasVersion, VersionPolicy versionPolicy, NuGetVersion? exactVersion, VersionRange? allowedVersionRange, bool? includePrerelease, string? prereleaseChannel)
private Rule(List<string> packages, bool hasVersion, VersionPolicy versionPolicy, NuGetVersion? exactVersion, VersionRange? allowedVersionRange, bool? includePrerelease, string? prereleaseChannel, PublishCooldown? publishCooldown)
{
m_packagePatterns = packages.Select(CreateWildcardRegex).ToList();
HasVersion = hasVersion;
Expand All @@ -328,6 +346,7 @@ private Rule(List<string> packages, bool hasVersion, VersionPolicy versionPolicy
AllowedVersionRange = allowedVersionRange;
IncludePrerelease = includePrerelease;
PrereleaseChannel = prereleaseChannel;
PublishCooldownPolicy = publishCooldown;
}

private readonly List<Regex> m_packagePatterns;
Expand All @@ -340,6 +359,13 @@ internal sealed class EffectiveRule
public VersionRange? AllowedVersionRange { get; set; }
public bool IncludePrerelease { get; set; }
public string? PrereleaseChannel { get; set; }
public PublishCooldown PublishCooldownPolicy { get; set; } = PublishCooldown.Weekly;
}

internal enum PublishCooldown
{
Weekly,
None
}

internal enum VersionPolicy
Expand Down Expand Up @@ -452,7 +478,7 @@ internal static class VersionResolver
{
var filteredCandidates = candidates
.Where(candidate => candidate.Listed)
.Where(candidate => candidate.PublishedUtc.HasValue && candidate.PublishedUtc.Value <= cutoffUtc)
.Where(candidate => rule.PublishCooldownPolicy == PublishCooldown.None || (candidate.PublishedUtc.HasValue && candidate.PublishedUtc.Value <= cutoffUtc))
.Where(candidate => candidate.Version.CompareTo(currentVersion) > 0)
.Where(candidate => IsPrereleaseAllowed(candidate.Version, rule))
.Where(candidate => IsAllowedByPolicy(candidate.Version, currentVersion, rule))
Expand Down