diff --git a/conventions/update-nuget-packages/README.md b/conventions/update-nuget-packages/README.md
index 32a8b2d..c6a25d3 100644
--- a/conventions/update-nuget-packages/README.md
+++ b/conventions/update-nuget-packages/README.md
@@ -12,6 +12,7 @@ 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
@@ -19,7 +20,7 @@ The convention requires the target directory to be inside a git worktree and onl
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.
@@ -42,4 +43,8 @@ conventions:
version: '[8.0.0, 9.0.0)'
- packages: StackExchange.Redis
prerelease-channel: faithlife
+ - packages:
+ - Faithlife.*
+ - Logos.*
+ publish-cooldown: none
```
diff --git a/conventions/update-nuget-packages/convention.Tests.ps1 b/conventions/update-nuget-packages/convention.Tests.ps1
index af00647..2f18b32 100644
--- a/conventions/update-nuget-packages/convention.Tests.ps1
+++ b/conventions/update-nuget-packages/convention.Tests.ps1
@@ -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 @'
+
+
+
+
+
+
+'@
+
+ 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
diff --git a/conventions/update-nuget-packages/convention.cs b/conventions/update-nuget-packages/convention.cs
index fd95502..bbcf393 100644
--- a/conventions/update-nuget-packages/convention.cs
+++ b/conventions/update-nuget-packages/convention.cs
@@ -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;
@@ -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)
{
@@ -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));
@@ -319,7 +337,7 @@ private static Regex CreateWildcardRegex(string pattern)
return new Regex(regex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
- private Rule(List packages, bool hasVersion, VersionPolicy versionPolicy, NuGetVersion? exactVersion, VersionRange? allowedVersionRange, bool? includePrerelease, string? prereleaseChannel)
+ private Rule(List packages, bool hasVersion, VersionPolicy versionPolicy, NuGetVersion? exactVersion, VersionRange? allowedVersionRange, bool? includePrerelease, string? prereleaseChannel, PublishCooldown? publishCooldown)
{
m_packagePatterns = packages.Select(CreateWildcardRegex).ToList();
HasVersion = hasVersion;
@@ -328,6 +346,7 @@ private Rule(List packages, bool hasVersion, VersionPolicy versionPolicy
AllowedVersionRange = allowedVersionRange;
IncludePrerelease = includePrerelease;
PrereleaseChannel = prereleaseChannel;
+ PublishCooldownPolicy = publishCooldown;
}
private readonly List m_packagePatterns;
@@ -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
@@ -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))