Skip to content
Merged
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
8 changes: 4 additions & 4 deletions .github/workflows/dotnetcore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1
- name: Install .Net Core
uses: actions/setup-dotnet@v2.0.0
- uses: actions/checkout@v4
- name: Install .Net
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.405
dotnet-version: 10.0.x
- name: Install dotnet-script
run: dotnet tool install dotnet-script --tool-path dotnet-script-tool

Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,42 @@ deps --filter McMaster

> The filter is applied as an regular expression

### Minimum package age

Brand new package versions are sometimes best left to simmer for a little while. The `--min-age` (`-ma`) option makes `dotnet-deps` ignore versions that were published too recently.

```shell
deps --min-age 2d
```

The value is a number followed by an optional suffix.

| Value | Meaning |
| ----- | ------- |
| `2d` | Ignore versions published less than 2 days ago |
| `12h` | Ignore versions published less than 12 hours ago |
| `2` | Same as `2d`. Without a suffix we default to days |

When a newer version is held back, the version we *would* have picked is reported like this.

```shell
LightInject 7.0.1 7.0.1 (nuget.org) 🍺 (holding back 7.1.0 ⏳)
```

The minimum age can also be specified in the project file using the `PackagesMinimumAge` property.

```xml
<PropertyGroup>
<PackagesMinimumAge>2d</PackagesMinimumAge>
</PropertyGroup>
```

The `--min-age` option takes precedence over `PackagesMinimumAge` when both are present.

> `PackagesMinimumAge` must be declared in the same file as the package references. Just like for `<PackageReference>` nodes, `dotnet-deps` does not evaluate MSBuild and will not pick up the property from an imported file such as `Directory.Build.props`.

> Feeds that do not report a publish date for a version (some local and private feeds) will never have that version held back.

### Locked dependencies

If we should want to "lock" a dependency to a specific version, we can do that by adding the `Locked` attribute as shown here.
Expand Down
6 changes: 6 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature"
}
}
60 changes: 54 additions & 6 deletions src/Dotnet.Deps.Core/DependencyAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class DependencyAnalyzer

private bool allowPreReleasePackages;

private TimeSpan? minimumAge;

private AppConsole console = new AppConsole(TextWriter.Null, TextWriter.Null);

public DependencyAnalyzer WithRootFolder(string rootFolder)
Expand Down Expand Up @@ -56,6 +58,16 @@ public DependencyAnalyzer WithPreReleaseOption(bool allowPreReleasePackages)
return this;
}

/// <summary>
/// Specifies the minimum age a package version must have before it is considered for an update.
/// Takes precedence over the <c>PackagesMinimumAge</c> property in the project file.
/// </summary>
public DependencyAnalyzer WithMinimumAge(TimeSpan? minimumAge)
{
this.minimumAge = minimumAge;
return this;
}

public async Task<Result[]> Execute()
{
var projectCollectionLoader = new ProjectCollectionLoader(console);
Expand All @@ -70,11 +82,20 @@ public async Task<Result[]> Execute()

console.WriteNormal($"Found {allPackages.Length} package references across {projectCollection.ProjectFiles.Length} project(s)");

var latestVersions = await latestVersionProvider.GetLatestVersions(allPackageNames, rootFolder, allowPreReleasePackages);
var minimumAgeIsInEffect = minimumAge.HasValue || projectCollection.ProjectFiles.Any(pf => pf.MinimumPackageAge.HasValue);
var packageVersions = await latestVersionProvider.GetPackageVersions(allPackageNames, rootFolder, allowPreReleasePackages, minimumAgeIsInEffect);
var utcNow = DateTimeOffset.UtcNow;

foreach (var projectFile in projectCollection.ProjectFiles)
{
console.WriteHeader(projectFile.Path);

var effectiveMinimumAge = minimumAge ?? projectFile.MinimumPackageAge;
if (effectiveMinimumAge.HasValue && effectiveMinimumAge.Value > TimeSpan.Zero)
{
console.WriteNormal($"Ignoring package versions published less than {MinimumAge.Format(effectiveMinimumAge.Value)} ago ⏳");
}

foreach (var packageReference in projectFile.PackageReferences)
{
if (!Regex.IsMatch(packageReference.Name, filter))
Expand All @@ -95,32 +116,43 @@ public async Task<Result[]> Execute()

if (FloatRange.TryParse(packageVersion, out var floatRange))
{
if (latestVersions.TryGetValue(packageReference.Name, out var latestVersion))
if (packageVersions.TryGetValue(packageReference.Name, out var availableVersions))
{
var latestVersion = availableVersions.GetLatestVersion(effectiveMinimumAge, utcNow);

if (!latestVersion.IsValid)
{
console.WriteError($"Unable to find package {packageReference.Name} ({packageReference.Version})");
if (availableVersions.HasVersions)
{
console.WriteHighlighted($"{packageReference.Name} {packageReference.Version} - no version is older than {MinimumAge.Format(effectiveMinimumAge.Value)} ⏳");
}
else
{
console.WriteError($"Unable to find package {packageReference.Name} ({packageReference.Version})");
}
continue;
}

var heldBack = GetHeldBackSuffix(availableVersions, latestVersion, effectiveMinimumAge, utcNow);

if (!IsLatestVersion(floatRange, latestVersion.NugetVersion))
{
if (updateDependencies)
{
console.WriteHighlighted($"{packageReference.Name} {packageReference.Version} => {latestVersion.NugetVersion} ({latestVersion.Feed}) UPDATED 🍺");
console.WriteHighlighted($"{packageReference.Name} {packageReference.Version} => {latestVersion.NugetVersion} ({latestVersion.Feed}) UPDATED 🍺{heldBack}");
packageReference.Update(latestVersion.NugetVersion.ToString());
results.Add(new Result(floatRange.MinVersion.ToString(), latestVersion.NugetVersion.ToString(), true, latestVersion.Feed, projectFile.Path));
}
else
{
results.Add(new Result(floatRange.MinVersion.ToString(), latestVersion.NugetVersion.ToString(), false, latestVersion.Feed, projectFile.Path));
console.WriteHighlighted($"{packageReference.Name} {packageReference.Version} => {latestVersion.NugetVersion} ({latestVersion.Feed}) 😢");
console.WriteHighlighted($"{packageReference.Name} {packageReference.Version} => {latestVersion.NugetVersion} ({latestVersion.Feed}) 😢{heldBack}");
}
}
else
{
results.Add(new Result(floatRange.MinVersion.ToString(), latestVersion.NugetVersion.ToString(), true, latestVersion.Feed, projectFile.Path));
console.WriteSuccess($"{packageReference.Name} {packageReference.Version} {latestVersion.NugetVersion} ({latestVersion.Feed}) 🍺");
console.WriteSuccess($"{packageReference.Name} {packageReference.Version} {latestVersion.NugetVersion} ({latestVersion.Feed}) 🍺{heldBack}");
}
}
}
Expand All @@ -147,6 +179,22 @@ public async Task<Result[]> Execute()
return results.ToArray();
}

private static string GetHeldBackSuffix(PackageVersions availableVersions, LatestVersion latestVersion, TimeSpan? effectiveMinimumAge, DateTimeOffset utcNow)
{
if (!effectiveMinimumAge.HasValue || effectiveMinimumAge.Value <= TimeSpan.Zero)
{
return string.Empty;
}

var newestVersion = availableVersions.GetLatestVersion(null, utcNow);
if (newestVersion.IsValid && newestVersion.NugetVersion > latestVersion.NugetVersion)
{
return $" (holding back {newestVersion.NugetVersion} ⏳)";
}

return string.Empty;
}

private bool IsLatestVersion(FloatRange currentVersion, NuGetVersion latestVersion)
{
if (currentVersion.FloatBehavior == NuGetVersionFloatBehavior.None)
Expand Down
4 changes: 2 additions & 2 deletions src/Dotnet.Deps.Core/Dotnet.Deps.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<PackageTags>dotnet;cli;csx;nuget;tool</PackageTags>
<PackageProjectUrl>https://github.com/seesharper/dotnet-deps</PackageProjectUrl>
Expand All @@ -10,7 +10,7 @@
<RepositoryUrl>https://github.com/seesharper/dotnet-deps.git</RepositoryUrl>
<Authors>Bernhard Richter</Authors>
<Description>A simple library that can be used to analyze and update NuGet dependencies.</Description>
<Version>2.1.2</Version>
<Version>2.2.0</Version>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
Expand Down
65 changes: 65 additions & 0 deletions src/Dotnet.Deps.Core/MinimumAge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Globalization;

namespace Dotnet.Deps.Core
{
/// <summary>
/// Parses and formats the minimum age a package version must have before it is considered for an update.
/// </summary>
public static class MinimumAge
{
/// <summary>
/// Tries to parse a minimum age such as "2d" (two days), "12h" (twelve hours) or "2" (two days).
/// </summary>
public static bool TryParse(string value, out TimeSpan minimumAge)
{
minimumAge = TimeSpan.Zero;

if (string.IsNullOrWhiteSpace(value))
{
return false;
}

var trimmedValue = value.Trim();
var unit = 'd';
var lastCharacter = trimmedValue[trimmedValue.Length - 1];
if (!char.IsDigit(lastCharacter))
{
unit = char.ToLowerInvariant(lastCharacter);
trimmedValue = trimmedValue.Substring(0, trimmedValue.Length - 1).Trim();
}

if (!double.TryParse(trimmedValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var number) || number < 0)
{
return false;
}

if (unit == 'd' && number <= TimeSpan.MaxValue.TotalDays)
{
minimumAge = TimeSpan.FromDays(number);
return true;
}

if (unit == 'h' && number <= TimeSpan.MaxValue.TotalHours)
{
minimumAge = TimeSpan.FromHours(number);
return true;
}

return false;
}

/// <summary>
/// Formats a minimum age for display purposes.
/// </summary>
public static string Format(TimeSpan minimumAge)
{
if (minimumAge.TotalDays >= 1)
{
return $"{minimumAge.TotalDays.ToString("0.##", CultureInfo.InvariantCulture)} {(minimumAge.TotalDays == 1 ? "day" : "days")}";
}

return $"{minimumAge.TotalHours.ToString("0.##", CultureInfo.InvariantCulture)} {(minimumAge.TotalHours == 1 ? "hour" : "hours")}";
}
}
}
Loading
Loading