This commit is contained in:
David Pine 2022-10-21 20:32:16 -05:00 коммит произвёл GitHub
Родитель eeab704f6d
Коммит 623558498e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
69 изменённых файлов: 690 добавлений и 211 удалений

Просмотреть файл

@ -49,6 +49,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNet.GitHubActions", "src
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "non-lts", "non-lts", "{3241C58C-62C0-40DD-9B85-43A50641A3FA}"
ProjectSection(SolutionItems) = preProject
non-lts\Dockerfile = non-lts\Dockerfile
non-lts\project.json = non-lts\project.json
non-lts\ReallyOld.xproj = non-lts\ReallyOld.xproj
EndProjectSection

29
non-lts/Dockerfile Normal file
Просмотреть файл

@ -0,0 +1,29 @@
# This file is intentionally not valid.
# Its only purpose is for dog fooding.
FROM mcr.microsoft.com/dotnet/aspnet:3.1.30-bionic AS build-env
WORKDIR /App
# Copy everything
COPY . ./
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine3.16
# Restore as distinct layers
RUN dotnet restore
FROM mcr.microsoft.com/dotnet/framework/sdk:4.7.1
# Build and publish a release
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/azure/bits:6.0
COPY --from=mcr.microsoft.com/dotnet/framework/runtime:3.5-20221011-windowsservercore-ltsc2019 /usr/share/dotnet/shared
FROM mcr.microsoft.com/dotnet/framework/sdk:4.8-20221011-windowsservercore-ltsc2022
# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /App
COPY --from=build-env /App/out .
ENTRYPOINT ["dotnet", "DotNet.Docker.dll"]

Просмотреть файл

@ -3,16 +3,10 @@
namespace DotNet.GitHub;
public class DefaultResilientGitHubClientFactory : IResilientGitHubClientFactory
public sealed class DefaultResilientGitHubClientFactory : IResilientGitHubClientFactory
{
readonly ResilientGitHubClientFactory _clientFactory;
public DefaultResilientGitHubClientFactory(
ResilientGitHubClientFactory clientFactory) =>
_clientFactory = clientFactory;
public IGitHubClient Create(string token) =>
_clientFactory.Create(
productHeaderValue: GitHubProduct.Header,
credentials: new(token));
new GitHubClient(
productInformation: GitHubProduct.Header,
credentialStore: new InMemoryCredentialStore(new(token)));
}

Просмотреть файл

@ -8,11 +8,11 @@
<ItemGroup>
<PackageReference Include="MarkdownBuilder" Version="0.2.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
<PackageReference Include="Octokit" Version="1.0.1" />
<PackageReference Include="Octokit.Extensions" Version="1.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" />
<PackageReference Include="Octokit" Version="4.0.0" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -3,7 +3,7 @@
namespace DotNet.GitHub;
public class GitHubGraphQLClient
public sealed class GitHubGraphQLClient
{
const string _issueQuery = @"query($search_value: String!) {
search(type: ISSUE, query: $search_value, first: 10) {

Просмотреть файл

@ -1,54 +1,49 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace DotNet.GitHub;
public class GitHubIssueService : IGitHubIssueService
{
readonly IResilientGitHubClientFactory _clientFactory;
readonly IGitHubLabelService _gitHubLabelService;
readonly ILogger<GitHubIssueService> _logger;
public GitHubIssueService(
IResilientGitHubClientFactory clientFactory,
IGitHubLabelService gitHubLabelService,
ILogger<GitHubIssueService> logger) =>
(_clientFactory, _gitHubLabelService, _logger) = (clientFactory, gitHubLabelService, logger);
public async ValueTask<Issue> PostIssueAsync(
string owner, string name, string token, NewIssue newIssue)
{
var issuesClient = GetIssuesClient(token);
var label = await _gitHubLabelService.GetOrCreateLabelAsync(owner, name, token);
newIssue.Labels.Add(label.Name);
var issue = await issuesClient.Create(owner, name, newIssue);
_logger.LogInformation($"Issue created: {issue.HtmlUrl}");
return issue;
}
public async ValueTask<Issue> UpdateIssueAsync(
string owner, string name, string token, long number, IssueUpdate issueUpdate)
{
var issuesClient = GetIssuesClient(token);
// The GitHub GraphQL API returns a long for the issue Id.
// The GitHub REST API expects an int for the issue Id.
var issue = await issuesClient.Update(
owner, name, unchecked((int)number), issueUpdate);
_logger.LogInformation($"Issue updated: {issue.HtmlUrl}");
return issue;
}
IIssuesClient GetIssuesClient(string token)
{
var client = _clientFactory.Create(token);
return client.Issue;
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace DotNet.GitHub;
public sealed class GitHubIssueService : IGitHubIssueService
{
readonly IResilientGitHubClientFactory _clientFactory;
readonly IGitHubLabelService _gitHubLabelService;
readonly ILogger<GitHubIssueService> _logger;
public GitHubIssueService(
IResilientGitHubClientFactory clientFactory,
IGitHubLabelService gitHubLabelService,
ILogger<GitHubIssueService> logger) =>
(_clientFactory, _gitHubLabelService, _logger) = (clientFactory, gitHubLabelService, logger);
public async ValueTask<Issue> PostIssueAsync(
string owner, string name, string token, NewIssue newIssue)
{
var label = await _gitHubLabelService.GetOrCreateLabelAsync(owner, name, token);
newIssue.Labels.Add(label.Name);
var issuesClient = GetIssuesClient(token);
var issue = await issuesClient.Create(owner, name, newIssue);
_logger.LogInformation($"Issue created: {issue.HtmlUrl}");
return issue;
}
public async ValueTask<Issue> UpdateIssueAsync(
string owner, string name, string token, long number, IssueUpdate issueUpdate)
{
var issuesClient = GetIssuesClient(token);
// The GitHub GraphQL API returns a long for the issue Id.
// The GitHub REST API expects an int for the issue Id.
var issue = await issuesClient.Update(
owner, name, unchecked((int)number), issueUpdate);
_logger.LogInformation($"Issue updated: {issue.HtmlUrl}");
return issue;
}
IIssuesClient GetIssuesClient(string token) => _clientFactory.Create(token).Issue;
}

Просмотреть файл

@ -6,7 +6,7 @@ namespace DotNet.GitHub;
public static class GitHubProduct
{
static readonly string _name = "DotNetVersionSweeper";
static readonly string _version = "1.0";
static readonly string _version = "1.1";
public static ProductHeaderValue Header { get; } = new(_name, _version);
}

Просмотреть файл

@ -12,5 +12,5 @@ global using Microsoft.Extensions.Caching.Memory;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Logging;
global using Octokit;
global using Octokit.Extensions;
global using Octokit.Internal;
global using ModelProject = DotNet.Models.Project;

Просмотреть файл

@ -66,7 +66,7 @@ public static class ModelExtensions
$"you can optionally configure to ignore these results in future automation executions. " +
$"Create a (or update the) *dotnet-versionsweeper.json* file at the root of the repository and " +
$"add an `ignore` entry following the " +
$"[globbing patterns detailed here](https://docs.microsoft.com/dotnet/api/microsoft.extensions.filesystemglobbing.matcher#remarks).");
$"[globbing patterns detailed here](https://learn.microsoft.com/dotnet/core/extensions/file-globbing).");
document.AppendCode("json", @"{
""ignore"": [
@ -76,6 +76,79 @@ public static class ModelExtensions
return document.ToString();
}
public static string ToMarkdownBody(
this ISet<DockerfileSupportReport> dfsr,
string tfm,
string rootDirectory,
string branch)
{
IMarkdownDocument document = new MarkdownDocument();
document.AppendParagraph(
"The following Dockerfile(s) target a .NET version which is no longer supported. " +
"This is an auto-generated issue, detailed and discussed in [dotnet/docs#22271](https://github.com/dotnet/docs/issues/22271).");
var tfmSupport =
dfsr.First()
.TargetFrameworkMonikerSupports
.First(tfms => tfms.TargetFrameworkMoniker == tfm);
document.AppendTable(
new MarkdownTableHeader(
new("Target version"),
new("End of life"),
new("Release notes"),
new("Nearest LTS TFM version")),
new[]
{
new MarkdownTableRow(
$"`{tfmSupport.TargetFrameworkMoniker}`",
tfmSupport.Release.EndOfLifeDate.HasValue
? $"{tfmSupport.Release.EndOfLifeDate:MMMM, dd yyyy}" : "N/A",
new MarkdownLink(
$"{tfmSupport.TargetFrameworkMoniker} release notes", tfmSupport.Release.ReleaseNotesUrl)
.ToString(),
$"`{tfmSupport.NearestLtsVersion}`")
});
document.AppendList(
new MarkdownList(
dfsr.SelectMany(sr => sr.TargetFrameworkMonikerSupports.Select(tfms => (sr.Dockerfile, tfms)))
.OrderBy(t => t.Dockerfile.FullPath)
.Select(t =>
{
var relativePath =
Path.GetRelativePath(rootDirectory, t.Dockerfile.FullPath);
// TODO: 1
var lineNumberFileReference =
$"../blob/{branch}/{relativePath.Replace("\\", "/")}#L{1}"
.EscapeUriString();
var name = relativePath.ShrinkPath("...");
// Must force anchor link, as GitHub assumes site-relative links.
var anchor = $"<a href='{lineNumberFileReference}' title='{name} at line number {1:#,0}'>{name}</a>";
return new MarkdownCheckListItem(false, anchor);
})));
document.AppendParagraph(
"Consider upgrading Dockerfile images to either the current release, or the nearest LTS TFM version.");
document.AppendParagraph(
$"If any of these Dockerfile(s) listed in this issue are intentionally targeting an unsupported version, " +
$"you can optionally configure to ignore these results in future automation executions. " +
$"Create a (or update the) *dotnet-versionsweeper.json* file at the root of the repository and " +
$"add an `ignore` entry following the " +
$"[globbing patterns detailed here](https://learn.microsoft.com/dotnet/core/extensions/file-globbing).");
document.AppendCode("json", @"{
""ignore"": [
""**/path/to/Dockerfile""
]
}");
return document.ToString();
}
public static bool TryCreateIssueContent(
this ISet<ModelProject> projects,
string rootDirectory,
@ -127,7 +200,7 @@ public static class ModelExtensions
$"you can optionally configure to ignore these results in future automation executions. " +
$"Create a (or update the) *dotnet-versionsweeper.json* file at the root of the repository and " +
$"add an `ignore` entry following the " +
$"[globbing patterns detailed here](https://docs.microsoft.com/dotnet/api/microsoft.extensions.filesystemglobbing.matcher#remarks).");
$"[globbing patterns detailed here](https://learn.microsoft.com/dotnet/core/extensions/file-globbing).");
document.AppendCode("json", @"{
""ignore"": [

Просмотреть файл

@ -3,7 +3,7 @@
namespace DotNet.GitHub;
public class RateLimitAwareQueue
public sealed class RateLimitAwareQueue
{
const int DelayBetweenPostCalls = 1_000;

Просмотреть файл

@ -11,7 +11,6 @@ public static class ServiceCollectionExtensions
services.AddHttpClient<GitHubGraphQLClient>();
return services
.AddSingleton<ResilientGitHubClientFactory>()
.AddSingleton<IResilientGitHubClientFactory, DefaultResilientGitHubClientFactory>()
.AddSingleton<GitHubGraphQLClient>()
.AddSingleton<RateLimitAwareQueue>()

Просмотреть файл

@ -5,7 +5,7 @@ using System.Text;
namespace DotNet.GitHubActions;
public class JobService : IJobService
public sealed class JobService : IJobService
{
/// <inheritdoc />
public void AddPath(string inputPath)

Просмотреть файл

@ -0,0 +1,83 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace DotNet.IO;
public sealed class DockerfileReader : IDockerfileReader
{
static readonly RegexOptions _options =
RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.ExplicitCapture;
static readonly Regex _fromInstructionRegex =
new(@"FROM (?<image>.+?dotnet.+?):(?<tag>.+?)[\s|\n]", _options);
static readonly Regex _copyInstructionRegex =
new(@"COPY --from=(?<image>.+?dotnet.+?):(?<tag>.+?)[\s|\n]", _options);
public async ValueTask<Dockerfile> ReadDockerfileAsync(string dockerfilePath)
{
Dockerfile dockerfile = new()
{
FullPath = dockerfilePath
};
if (SystemFile.Exists(dockerfilePath))
{
var dockerfileContent = await SystemFile.ReadAllTextAsync(dockerfilePath);
var fromMatches = _fromInstructionRegex.Matches(dockerfileContent);
foreach (Match match in fromMatches)
{
dockerfile = ParseMatch(dockerfile, dockerfileContent, match);
}
var copyMatches = _copyInstructionRegex.Matches(dockerfileContent);
foreach (Match match in copyMatches)
{
dockerfile = ParseMatch(dockerfile, dockerfileContent, match);
}
}
return dockerfile;
static Dockerfile ParseMatch(Dockerfile dockerfile, string dockerfileContent, Match match)
{
var group = match.Groups["tag"];
var (index, tag) = (group.Index, group.Value);
var image = match.Groups["image"].Value;
if (image is not null && tag is not null)
{
if (dockerfile.ImageDetails is null)
{
dockerfile = dockerfile with
{
ImageDetails = new HashSet<ImageDetails>()
};
}
var lineNumber = GetLineNumberFromIndex(dockerfileContent, index);
var isFramework = image.Contains("framework");
tag = tag.Contains('-') ? tag.Split("-")[0] : tag;
var firstNumber = int.TryParse(tag[0].ToString(), out var number) ? number : -1;
var tfm = isFramework switch
{
true => $"net{tag.Replace(".", "")}",
false when firstNumber < 4 => $"netcoreapp{tag}",
_ => $"net{tag}"
};
dockerfile.ImageDetails.Add(
new ImageDetails(image, tag, tfm, lineNumber));
}
return dockerfile;
}
}
static int GetLineNumberFromIndex(string content, int index)
{
var lineNumber = 1;
for (var i = 0; i < index; ++i)
{
if (content[i] == '\n') ++lineNumber;
}
return lineNumber;
}
}

Просмотреть файл

@ -0,0 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace DotNet.IO;
public interface IDockerfileReader
{
ValueTask<Dockerfile> ReadDockerfileAsync(string projectPath);
}

Просмотреть файл

@ -9,5 +9,6 @@ public static class ServiceCollectionExtensions
this IServiceCollection services) =>
services
.AddSingleton<IProjectFileReader, ProjectFileReader>()
.AddSingleton<ISolutionFileReader, SolutionFileReader>();
.AddSingleton<ISolutionFileReader, SolutionFileReader>()
.AddSingleton<IDockerfileReader, DockerfileReader>();
}

Просмотреть файл

@ -3,7 +3,7 @@
namespace DotNet.IO;
public class ProjectFileReader : IProjectFileReader
public sealed class ProjectFileReader : IProjectFileReader
{
static readonly RegexOptions _options =
RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.ExplicitCapture;
@ -48,7 +48,7 @@ public class ProjectFileReader : IProjectFileReader
static Project ParseJson(Project project, string projectContent)
{
var projectJson = projectContent.FromJson<ProjectJson>();
if (projectJson is null or { Frameworks: { Count: 0 } })
if (projectJson is null or { Frameworks.Count: 0 })
{
return project;
}

Просмотреть файл

@ -3,12 +3,12 @@
namespace DotNet.IO;
internal class ProjectJson
internal sealed class ProjectJson
{
[JsonPropertyName("frameworks")]
public Dictionary<string, Framework> Frameworks { get; set; } = new();
}
internal class Framework
internal sealed class Framework
{
}

Просмотреть файл

@ -3,7 +3,7 @@
namespace DotNet.IO;
public class SolutionFileReader : ISolutionFileReader
public sealed class SolutionFileReader : ISolutionFileReader
{
static readonly RegexOptions _options =
RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.ExplicitCapture;

Просмотреть файл

@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace DotNet.Models;
public record Dockerfile
{
/// <summary>
/// The fully qualified path of the <i>Dockerfile</i>.
/// </summary>
public string FullPath { get; init; } = default!;
/// <summary>
/// The image details for each <c>FROM</c> instruction in the <i>Dockerfile</i>.
/// </summary>
public ISet<ImageDetails>? ImageDetails { get; init; }
}

Просмотреть файл

@ -0,0 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace DotNet.Models;
public record DockerfileSupportReport(
Dockerfile Dockerfile,
HashSet<TargetFrameworkMonikerSupport> TargetFrameworkMonikerSupports);

Просмотреть файл

@ -26,7 +26,7 @@ public record FrameworkRelease(
public SupportPhase SupportPhase => EndOfLifeDate switch
{
var date when date is null && Version == "4.8" => SupportPhase.Current,
var date when date > DateTime.Now => SupportPhase.LTS,
var date when date > DateTimeOffset.UtcNow => SupportPhase.LTS,
_ => SupportPhase.EOL
};

Просмотреть файл

@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace DotNet.Models;
public readonly record struct ImageDetails(
string Image,
string Tag,
string TargetFrameworkMoniker,
int LineNumber);

Просмотреть файл

@ -6,12 +6,3 @@ namespace DotNet.Models;
public record ProjectSupportReport(
Project Project,
HashSet<TargetFrameworkMonikerSupport> TargetFrameworkMonikerSupports);
public record TargetFrameworkMonikerSupport(
string TargetFrameworkMoniker,
string Version,
bool IsUnsupported,
IRelease Release)
{
public string NearestLtsVersion { get; set; } = null!;
}

Просмотреть файл

@ -3,13 +3,15 @@
namespace DotNet.Models;
public class ReleaseFactory
public sealed class ReleaseFactory
{
public static IRelease Create<TSource>(
TSource source,
Func<TSource, string> toString,
string tfm, SupportPhase supportPhase,
DateTime? endOfLifeDate, string releaseNotesUrl) =>
string tfm,
SupportPhase supportPhase,
DateTime? endOfLifeDate,
string releaseNotesUrl) =>
new ReleaseWrapper<TSource>(() => toString(source))
{
TargetFrameworkMoniker = tfm,

Просмотреть файл

@ -3,7 +3,7 @@
namespace DotNet.Models;
public class Solution
public sealed class Solution
{
public string FullPath { get; set; } = null!;

Просмотреть файл

@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace DotNet.Models;
public record TargetFrameworkMonikerSupport(
string TargetFrameworkMoniker,
string Version,
bool IsUnsupported,
IRelease Release)
{
public string NearestLtsVersion { get; set; } = null!;
}

Просмотреть файл

@ -3,7 +3,7 @@
namespace DotNet.Releases;
public class CoreReleaseIndexService : ICoreReleaseIndexService
public sealed class CoreReleaseIndexService : ICoreReleaseIndexService
{
const string NetCoreKey = nameof(NetCoreKey);
@ -19,11 +19,11 @@ public class CoreReleaseIndexService : ICoreReleaseIndexService
{
var products = await ProductCollection.GetAsync();
var map = new Dictionary<Product, IReadOnlyCollection<ProductRelease>>();
foreach (var product in products)
var map = new ConcurrentDictionary<Product, IReadOnlyCollection<ProductRelease>>();
await Parallel.ForEachAsync(products, async (product, token) =>
{
map[product] = await product.GetReleasesAsync();
}
});
return map.AsReadOnly();
});

Просмотреть файл

@ -41,7 +41,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Deployment.DotNet.Releases" Version="1.0.0-preview1.1.21116.1" />
<PackageReference Include="Microsoft.Deployment.DotNet.Releases" Version="1.0.0-preview4.1.22206.3" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />

Просмотреть файл

@ -6,5 +6,5 @@ namespace DotNet.Releases.Extensions;
internal static class DateTimeExtensions
{
internal static bool IsInTheFuture(this DateTime dateTime) =>
dateTime > DateTime.Now;
dateTime > DateTimeOffset.UtcNow;
}

Просмотреть файл

@ -12,5 +12,6 @@ public static class ServiceCollectionExtensions
.AddSingleton<ICoreReleaseIndexService, CoreReleaseIndexService>()
.AddSingleton<IFrameworkReleaseIndexService, FrameworkReleaseIndexService>()
.AddSingleton<IFrameworkReleaseService, FrameworkReleaseService>()
.AddSingleton<IUnsupportedProjectReporter, UnsupportedProjectReporter>();
.AddSingleton<IUnsupportedProjectReporter, UnsupportedProjectReporter>()
.AddSingleton<IUnsupportedDockerfileReporter, UnsupportedDockerfileReporter>();
}

Просмотреть файл

@ -3,10 +3,10 @@
namespace DotNet.Releases;
public class FrameworkReleaseIndexService : IFrameworkReleaseIndexService
public sealed class FrameworkReleaseIndexService : IFrameworkReleaseIndexService
{
/// <summary>
/// Private sourhttps://github.com/dotnet/website-resources/tree/master/data/dotnet-framework-releases
/// https://github.com/dotnet/website-resources/tree/master/data/dotnet-framework-releases
/// </summary>
public HashSet<string> FrameworkReseaseFileNames { get; } = new()
{

Просмотреть файл

@ -3,7 +3,7 @@
namespace DotNet.Releases;
public class FrameworkReleaseService : IFrameworkReleaseService
public sealed class FrameworkReleaseService : IFrameworkReleaseService
{
readonly IMemoryCache _cache;
readonly IFrameworkReleaseIndexService _indexService;

Просмотреть файл

@ -13,7 +13,7 @@ public interface IFrameworkReleaseService
var releases = await sequence.ToListAsync();
static bool IsOutOfSupport(FrameworkRelease release) =>
release.SupportPhase == SupportPhase.EOL || release.EndOfLifeDate?.Date <= DateTime.Now.Date;
release.SupportPhase == SupportPhase.EOL || release.EndOfLifeDate?.Date <= DateTimeOffset.UtcNow.Date;
var orderedReleases = releases?
.Where(release => release is not null)

Просмотреть файл

@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
global using System.Collections.Concurrent;
global using System.Reflection;
global using DotNet.Extensions;
global using DotNet.Models;

Просмотреть файл

@ -3,7 +3,7 @@
namespace DotNet.Releases;
public class LabeledVersion : IComparable<LabeledVersion?>
public sealed class LabeledVersion : IComparable<LabeledVersion?>
{
private readonly Version? _parsedVersion;

Просмотреть файл

@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace DotNet.Releases;
public interface IUnsupportedDockerfileReporter
{
IAsyncEnumerable<DockerfileSupportReport> ReportAsync(
Dockerfile dockerfile, int outOfSupportWithinDays);
}

Просмотреть файл

@ -0,0 +1,94 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace DotNet.Releases;
internal sealed class UnsupportedDockerfileReporter : UnsupportedReporterBase, IUnsupportedDockerfileReporter
{
readonly ICoreReleaseIndexService _coreReleaseIndexService;
readonly IFrameworkReleaseService _frameworkReleaseService;
public UnsupportedDockerfileReporter(
ICoreReleaseIndexService coreReleaseIndexService,
IFrameworkReleaseService frameworkReleaseService) =>
(_coreReleaseIndexService, _frameworkReleaseService) =
(coreReleaseIndexService, frameworkReleaseService);
async IAsyncEnumerable<DockerfileSupportReport> IUnsupportedDockerfileReporter.ReportAsync(
Dockerfile dockerfile, int outOfSupportWithinDays)
{
HashSet<TargetFrameworkMonikerSupport> resultingSupports = new();
DateTime outOfSupportWithinDate = DateTimeOffset.UtcNow.Date.AddDays(outOfSupportWithinDays);
var products = await _coreReleaseIndexService.GetReleasesAsync();
foreach (var product in products.Keys)
{
var tfmSupports =
dockerfile.ImageDetails!.Select(
details => TryEvaluateDotNetSupport(
details.TargetFrameworkMoniker, product.ProductVersion,
product, outOfSupportWithinDate, out var tfmSupport)
? tfmSupport : null)
.Where(tfmSupport => tfmSupport is not null);
if (tfmSupports.Any())
{
var supports = await Task.WhenAll(
tfmSupports.Where(support => support?.IsUnsupported ?? false)
.Select(
async support =>
{
var release = await _coreReleaseIndexService.GetNextLtsVersionAsync(
product.LatestReleaseVersion.ToString());
return support! with
{
NearestLtsVersion = release!.GetTargetFrameworkMoniker()
};
}));
foreach (var support in supports)
{
resultingSupports.Add(support);
}
}
}
await foreach (var frameworkRelease
in _frameworkReleaseService.GetAllReleasesAsync())
{
var tfmSupports =
dockerfile.ImageDetails!.Select(
details => TryEvaluateDotNetFrameworkSupport(
details.TargetFrameworkMoniker, frameworkRelease!.Version,
frameworkRelease, outOfSupportWithinDate, out var tfmSupport)
? tfmSupport : null)
.Where(tfmSupport => tfmSupport is not null);
if (tfmSupports.Any())
{
var supports = await Task.WhenAll(
tfmSupports.Where(support => support?.IsUnsupported ?? false)
.Select(
async support =>
{
var release = await _frameworkReleaseService.GetNextLtsVersionAsync(
(LabeledVersion)frameworkRelease.Version);
return support! with
{
NearestLtsVersion = release!.TargetFrameworkMoniker
};
}));
foreach (var support in supports)
{
resultingSupports.Add(support);
}
}
}
if (resultingSupports.Any())
yield return new(dockerfile, resultingSupports);
}
}

Просмотреть файл

@ -3,7 +3,7 @@
namespace DotNet.Releases;
internal class UnsupportedProjectReporter : IUnsupportedProjectReporter
internal sealed class UnsupportedProjectReporter : UnsupportedReporterBase, IUnsupportedProjectReporter
{
readonly ICoreReleaseIndexService _coreReleaseIndexService;
readonly IFrameworkReleaseService _frameworkReleaseService;
@ -18,14 +18,14 @@ internal class UnsupportedProjectReporter : IUnsupportedProjectReporter
Project project, int outOfSupportWithinDays)
{
HashSet<TargetFrameworkMonikerSupport> resultingSupports = new();
DateTime outOfSupportWithinDate = DateTime.Now.Date.AddDays(outOfSupportWithinDays);
DateTime outOfSupportWithinDate = DateTimeOffset.UtcNow.Date.AddDays(outOfSupportWithinDays);
var products = await _coreReleaseIndexService.GetReleasesAsync();
foreach (var product in products.Keys)
{
var tfmSupports =
project.Tfms.Select(
tfm => TryEvaluateReleaseSupport(
tfm => TryEvaluateDotNetSupport(
tfm, product.ProductVersion,
product, outOfSupportWithinDate, out var tfmSupport)
? tfmSupport : null)
@ -59,7 +59,7 @@ internal class UnsupportedProjectReporter : IUnsupportedProjectReporter
{
var tfmSupports =
project.Tfms.Select(
tfm => TryEvaluateReleaseSupport(
tfm => TryEvaluateDotNetFrameworkSupport(
tfm, frameworkRelease!.Version,
frameworkRelease, outOfSupportWithinDate, out var tfmSupport)
? tfmSupport : null)
@ -91,55 +91,4 @@ internal class UnsupportedProjectReporter : IUnsupportedProjectReporter
if (resultingSupports.Any())
yield return new(project, resultingSupports);
}
static bool TryEvaluateReleaseSupport(
string tfm, string version,
Product product,
DateTime outOfSupportWithinDate,
out TargetFrameworkMonikerSupport? tfmSupport)
{
var release = ReleaseFactory.Create(
product,
pr => $"{pr.ProductName} {pr.ProductVersion}",
product.ProductName switch
{
".NET" => $"net{product.ProductVersion}",
".NET Core" => $"netcoreapp{product.ProductVersion}",
_ => product.ProductVersion
},
product.SupportPhase,
product.EndOfLifeDate,
product.ReleasesJson.ToString());
if (TargetFrameworkMonikerMap.RawMapsToKnown(tfm, release.TargetFrameworkMoniker))
{
var isOutOfSupport = product.IsOutOfSupport() ||
product.EndOfLifeDate <= outOfSupportWithinDate;
tfmSupport = new(tfm, version, isOutOfSupport, release);
return true;
}
tfmSupport = default;
return false;
}
static bool TryEvaluateReleaseSupport(
string tfm, string version,
IRelease release,
DateTime outOfSupportWithinDate,
out TargetFrameworkMonikerSupport? tfmSupport)
{
if (TargetFrameworkMonikerMap.RawMapsToKnown(tfm, release.TargetFrameworkMoniker))
{
var isOutOfSupport = release.SupportPhase == SupportPhase.EOL ||
release.EndOfLifeDate?.Date <= outOfSupportWithinDate;
tfmSupport = new(tfm, version, isOutOfSupport, release);
return true;
}
tfmSupport = default;
return false;
}
}

Просмотреть файл

@ -0,0 +1,54 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace DotNet.Releases;
internal class UnsupportedReporterBase
{
protected static bool TryEvaluateDotNetSupport(
string tfm,
string version,
Product product,
DateTime outOfSupportWithinDate,
out TargetFrameworkMonikerSupport? tfmSupport)
{
var release = ReleaseFactory.Create(
product,
pr => $"{pr.ProductName} {pr.ProductVersion}",
product.GetTargetFrameworkMoniker(),
product.SupportPhase,
product.EndOfLifeDate,
product.ReleasesJson.ToString());
if (TargetFrameworkMonikerMap.RawMapsToKnown(tfm, release.TargetFrameworkMoniker))
{
var isOutOfSupport = product.IsOutOfSupport() ||
product.EndOfLifeDate <= outOfSupportWithinDate;
tfmSupport = new(tfm, version, isOutOfSupport, release);
return true;
}
tfmSupport = default;
return false;
}
protected static bool TryEvaluateDotNetFrameworkSupport(
string tfm, string version,
IRelease release,
DateTime outOfSupportWithinDate,
out TargetFrameworkMonikerSupport? tfmSupport)
{
if (TargetFrameworkMonikerMap.RawMapsToKnown(tfm, release.TargetFrameworkMoniker))
{
var isOutOfSupport = release.SupportPhase == SupportPhase.EOL ||
release.EndOfLifeDate?.Date <= outOfSupportWithinDate;
tfmSupport = new(tfm, version, isOutOfSupport, release);
return true;
}
tfmSupport = default;
return false;
}
}

Просмотреть файл

@ -9,7 +9,7 @@ sealed class Discovery
/// Returns a list of solutions, each solution contains projects. Also returns a mapping
/// of orphaned projects that do not belong to solutions, but match the search patterns.
/// </summary>
internal static async Task<(ISet<Solution> Solutions, ISet<ModelProject> OrphanedProjects, VersionSweeperConfig Config)>
internal static async Task<(IImmutableSet<Solution> Solutions, IImmutableSet<ModelProject> OrphanedProjects, VersionSweeperConfig Config)>
FindSolutionsAndProjectsAsync(
IServiceProvider services,
IJobService job,
@ -32,7 +32,7 @@ sealed class Discovery
await Task.WhenAll(
projectMatcher.GetResultsInFullPath(options.Directory)
.ForEachAsync(
Environment.ProcessorCount,
ProcessorCount,
async path =>
{
var project = await projectReader.ReadProjectAsync(path);
@ -44,7 +44,7 @@ sealed class Discovery
}),
solutionMatcher.GetResultsInFullPath(options.Directory)
.ForEachAsync(
Environment.ProcessorCount,
ProcessorCount,
async path =>
{
var solution = await solutionReader.ReadSolutionAsync(path);
@ -75,10 +75,10 @@ sealed class Discovery
})
);
var solutionSet = solutions.ToHashSet();
var solutionSet = solutions.ToImmutableHashSet();
var orphanedProjectSet =
projects.Except(solutions.SelectMany(sln => sln.Projects))
.ToHashSet();
.ToImmutableHashSet();
job.Info($"Discovered {solutionSet.Count} solutions and {orphanedProjectSet.Count} orphaned projects.");
@ -89,4 +89,32 @@ sealed class Discovery
Config: config
);
}
internal static async Task<IImmutableSet<Dockerfile>> FindDockerfilesAsync(
IServiceProvider services,
IJobService job,
Options options)
{
var dockerfileReader = services.GetRequiredService<IDockerfileReader>();
ConcurrentBag<Dockerfile> dockerfiles = new();
var dockerfileMatcher = new Matcher().AddInclude("**/Dockerfile");
await dockerfileMatcher.GetResultsInFullPath(options.Directory)
.ForEachAsync(
ProcessorCount,
async path =>
{
var dockerfile = await dockerfileReader.ReadDockerfileAsync(path);
if (dockerfile is { ImageDetails.Count: > 0 })
{
dockerfiles.Add(dockerfile);
foreach (var imageDetail in dockerfile.ImageDetails)
{
job.Info($"Parsed TFM(s): '{imageDetail.TargetFrameworkMoniker}' on line {imageDetail.LineNumber} in {path}.");
}
}
});
return dockerfiles.ToImmutableHashSet();
}
}

Просмотреть файл

@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
global using System.Collections.Concurrent;
global using System.Collections.Immutable;
global using System.Text;
global using System.Text.Json.Serialization;
global using CommandLine;

Просмотреть файл

@ -3,7 +3,7 @@
namespace DotNet.VersionSweeper;
public class Options
public sealed class Options
{
string _repositoryName = null!;
string _branchName = null!;

Просмотреть файл

@ -23,9 +23,13 @@ static async Task StartSweeperAsync(Options options, IServiceProvider services,
var (solutions, orphanedProjects, config) =
await Discovery.FindSolutionsAndProjectsAsync(services, job, options);
var (unsupportedProjectReporter, issueQueue, graphQLClient) =
var dockerfiles =
await Discovery.FindDockerfilesAsync(services, job, options);
var (unsupportedProjectReporter, unsupportedDockerfileReporter, issueQueue, graphQLClient) =
services.GetRequiredServices
<IUnsupportedProjectReporter, RateLimitAwareQueue, GitHubGraphQLClient>();
<IUnsupportedProjectReporter, IUnsupportedDockerfileReporter,
RateLimitAwareQueue, GitHubGraphQLClient>();
static async Task CreateAndEnqueueAsync(
GitHubGraphQLClient client,
@ -74,19 +78,22 @@ static async Task StartSweeperAsync(Options options, IServiceProvider services,
HashSet<ModelProject> nonSdkStyleProjects = new();
Dictionary<string, HashSet<ProjectSupportReport>> tfmToProjectSupportReports =
new(StringComparer.OrdinalIgnoreCase);
Dictionary<string, HashSet<DockerfileSupportReport>> tfmToDockerfileSupportReports =
new(StringComparer.OrdinalIgnoreCase);
void AppendGrouping(
IGrouping<string, (TargetFrameworkMonikerSupport tfms, ProjectSupportReport psr)> grouping)
static void AppendGrouping<T>(
Dictionary<string, HashSet<T>> tfmToSupportReport,
IGrouping<string, (TargetFrameworkMonikerSupport tfms, T report)> grouping)
{
var key = grouping.Key;
if (!tfmToProjectSupportReports.ContainsKey(key))
if (!tfmToSupportReport.ContainsKey(key))
{
tfmToProjectSupportReports[key] = new();
tfmToSupportReport[key] = new();
}
foreach (var group in grouping)
foreach (var (_, report) in grouping)
{
tfmToProjectSupportReports[key].Add(group.psr);
tfmToSupportReport[key].Add(report);
}
}
@ -118,7 +125,7 @@ static async Task StartSweeperAsync(Options options, IServiceProvider services,
psr => psr.TargetFrameworkMonikerSupports, (psr, tfms) => (tfms, psr))
.GroupBy(t => t.tfms.TargetFrameworkMoniker))
{
AppendGrouping(grouping);
AppendGrouping(tfmToProjectSupportReports, grouping);
}
}
}
@ -140,12 +147,38 @@ static async Task StartSweeperAsync(Options options, IServiceProvider services,
reports.Select(tfms => (tfms, psr))
.GroupBy(t => t.tfms.TargetFrameworkMoniker))
{
AppendGrouping(grouping);
AppendGrouping(tfmToProjectSupportReports, grouping);
}
}
}
}
foreach (var dockerfile in dockerfiles)
{
await foreach (var supportReport in unsupportedDockerfileReporter.ReportAsync(
dockerfile, config.OutOfSupportWithinDays))
{
var reports = supportReport.TargetFrameworkMonikerSupports;
if (reports is { Count: > 0 } && reports.Any(r => r.IsUnsupported))
{
foreach (var grouping in
reports.Select(tfms => (tfms, supportReport))
.GroupBy(t => t.tfms.TargetFrameworkMoniker))
{
AppendGrouping(tfmToDockerfileSupportReports, grouping);
}
}
}
}
foreach (var (tfm, dockerfileSupportReports) in tfmToDockerfileSupportReports)
{
await CreateAndEnqueueAsync(
graphQLClient, issueQueue, job,
$"Upgrade from `{tfm}` to LTS (or current) image tag",
options, o => dockerfileSupportReports.ToMarkdownBody(tfm, o.Directory, o.Branch));
}
foreach (var (tfm, projectSupportReports) in tfmToProjectSupportReports)
{
await CreateAndEnqueueAsync(
@ -173,13 +206,13 @@ static async Task StartSweeperAsync(Options options, IServiceProvider services,
}
finally
{
Environment.Exit(0);
Exit(0);
}
}
parser.WithNotParsed(
errors => jobService.SetFailed(
string.Join(Environment.NewLine, errors.Select(error => error.ToString()))));
string.Join(NewLine, errors.Select(error => error.ToString()))));
await parser.WithParsedAsync(options => StartSweeperAsync(options, host.Services, jobService));
await host.RunAsync();

Просмотреть файл

@ -14,14 +14,16 @@ public static class ServiceProviderExtensions
provider.GetRequiredService<T2>()
);
public static (T1, T2, T3) GetRequiredServices<T1, T2, T3>(
public static (T1, T2, T3, T4) GetRequiredServices<T1, T2, T3, T4>(
this IServiceProvider provider)
where T1 : notnull
where T2 : notnull
where T3 : notnull =>
where T3 : notnull
where T4 : notnull =>
(
provider.GetRequiredService<T1>(),
provider.GetRequiredService<T2>(),
provider.GetRequiredService<T3>()
provider.GetRequiredService<T3>(),
provider.GetRequiredService<T4>()
);
}

Просмотреть файл

@ -14,8 +14,10 @@ namespace DotNet.VersionSweeper;
/// ]
/// }
/// </summary>
public class VersionSweeperConfig
public sealed class VersionSweeperConfig
{
private static VersionSweeperConfig? s_cachedConfig = null;
internal const string FileName = "dotnet-versionsweeper.json";
[JsonPropertyName("ignore")]
@ -27,8 +29,17 @@ public class VersionSweeperConfig
[JsonPropertyName("outOfSupportWithinDays")]
public int OutOfSupportWithinDays { get; init; } = 0;
/// <summary>
/// Returns the read configuration, or a default configuration if the file is not found.
/// The configuration is cached for the lifetime of the process.
/// </summary>
internal static async Task<VersionSweeperConfig> ReadAsync(string root, IJobService job)
{
if (s_cachedConfig is not null)
{
return s_cachedConfig;
}
try
{
var fullPath = Path.Combine(root, FileName);
@ -44,6 +55,8 @@ public class VersionSweeperConfig
job.Info($"Intended version sweeper type: {config.Type}");
job.Info($"Out of support within days: {config.OutOfSupportWithinDays}");
s_cachedConfig = config;
return config;
}
else

Просмотреть файл

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">

Просмотреть файл

@ -6,7 +6,7 @@ using Xunit;
namespace DotNet.Extensions.Tests;
public class ObjectExtensionsTests
public sealed class ObjectExtensionsTests
{
public static IEnumerable<object[]> FromJsonInput = new[]
{
@ -87,7 +87,7 @@ enum TestEnum
WaitWhat = 2
}
public class CustomName
public sealed class CustomName
{
[JsonPropertyName("test.value")] public string TestValue { get; init; }
}

Просмотреть файл

@ -5,7 +5,7 @@ using Xunit;
namespace DotNet.Extensions.Tests;
public class StringExtensionsTests
public sealed class StringExtensionsTests
{
[Fact]
public void FirstAndLastSegementsOfPathTest()

Просмотреть файл

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">

Просмотреть файл

@ -6,7 +6,7 @@ using Xunit;
namespace DotNet.GitHubActionsTests;
public class WorkflowCommandTests
public sealed class WorkflowCommandTests
{
public static IEnumerable<object[]> WorkflowCommandToStringInput = new[]
{

Просмотреть файл

@ -7,7 +7,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">

Просмотреть файл

@ -11,10 +11,10 @@ using Xunit;
namespace DotNet.GitHubTests;
public class GitHubLabelServiceTests
public sealed class GitHubLabelServiceTests
{
readonly ILogger<GitHubLabelService> _logger = new LoggerFactory().CreateLogger<GitHubLabelService>();
readonly MemoryCache _cache = new(Options.Create(new MemoryCacheOptions()));
readonly IMemoryCache _cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
[Fact]
public async Task GetOrCreateLabelAsyncCorrectlyReadsLabel()
@ -94,7 +94,10 @@ public class GitHubLabelServiceTests
}
}
public class TestLabel : Label
public sealed class TestLabel : Label
{
public TestLabel(string name) => Name = name;
public TestLabel(string name) :
base(id: 7, url: "", name, nodeId: "", color: "", description: "test description", @default: false)
{
}
}

Просмотреть файл

@ -11,7 +11,7 @@ using Xunit;
namespace DotNet.GitHubTests;
public class GraphQLRequestTests
public sealed class GraphQLRequestTests
{
readonly static JsonSerializerOptions _options = new()
{

Просмотреть файл

@ -3,7 +3,7 @@
namespace DotNet.IOTests;
class Constants
static class Constants
{
internal const string TestSolutionXml = @"
Microsoft Visual Studio Solution File, Format Version 12.00
@ -157,4 +157,31 @@ EndGlobal
]
}
}";
internal const string DockerfileWithMultipleTfms = @"FROM mcr.microsoft.com/dotnet/aspnet:3.1.30-bionic AS build-env
WORKDIR /App
# Copy everything
COPY . ./
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine3.16
# Restore as distinct layers
RUN dotnet restore
FROM mcr.microsoft.com/dotnet/framework/sdk:4.7.1
# Build and publish a release
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/azure/bits:6.0
COPY --from=mcr.microsoft.com/dotnet/framework/runtime:3.5-20221011-windowsservercore-ltsc2019 /usr/share/dotnet/shared
FROM mcr.microsoft.com/dotnet/framework/sdk:4.8-20221011-windowsservercore-ltsc2022
# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /App
COPY --from=build-env /App/out .
ENTRYPOINT [""dotnet"", ""DotNet.Docker.dll""]";
}

Просмотреть файл

@ -0,0 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using DotNet.IO;
using Xunit;
namespace DotNet.IOTests;
public sealed class DockerfileReaderTests
{
[Fact]
public async Task ReadDcokerfileAndParsesCorrectly()
{
var dockerfilePath = "Dockerfile";
try
{
await File.WriteAllTextAsync(dockerfilePath, Constants.DockerfileWithMultipleTfms);
IDockerfileReader sut = new DockerfileReader();
var dockerfile = await sut.ReadDockerfileAsync(dockerfilePath);
Assert.Equal(6, dockerfile.ImageDetails.Count);
Assert.Contains(dockerfile.ImageDetails, i => i.TargetFrameworkMoniker == "net35");
Assert.Contains(dockerfile.ImageDetails, i => i.TargetFrameworkMoniker == "net471");
Assert.Contains(dockerfile.ImageDetails, i => i.TargetFrameworkMoniker == "net48");
Assert.Contains(dockerfile.ImageDetails, i => i.TargetFrameworkMoniker == "netcoreapp3.1.30");
Assert.Contains(dockerfile.ImageDetails, i => i.TargetFrameworkMoniker == "net6.0");
Assert.Contains(dockerfile.ImageDetails, i => i.TargetFrameworkMoniker == "net7.0");
}
finally
{
File.Delete(dockerfilePath);
}
}
}

Просмотреть файл

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">

Просмотреть файл

@ -6,7 +6,7 @@ using Xunit;
namespace DotNet.IOTests;
public class ProjectFileReaderTests
public sealed class ProjectFileReaderTests
{
[Fact]
public async Task ReadProjectAndParseXmlCorrectly()

Просмотреть файл

@ -6,7 +6,7 @@ using Xunit;
namespace DotNet.IOTests;
public class SolutionFileReaderTests
public sealed class SolutionFileReaderTests
{
[Fact]
public async Task ReadSolutionAsyncTest()

Просмотреть файл

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">

Просмотреть файл

@ -6,7 +6,7 @@ using Xunit;
namespace DotNet.ModelsTests;
public class FrameworkReleaseTests
public sealed class FrameworkReleaseTests
{
[
Theory,

Просмотреть файл

@ -6,7 +6,7 @@ using Xunit;
namespace DotNet.ModelsTests;
public class ProjectTests
public sealed class ProjectTests
{
public static IEnumerable<object[]> NewProjectInput = new[]
{

Просмотреть файл

@ -7,7 +7,7 @@ using Xunit;
namespace DotNet.ModelsTests;
public class ReleaseFactoryTests
public sealed class ReleaseFactoryTests
{
[Fact]
public void CreateTest()

Просмотреть файл

@ -11,7 +11,7 @@ using Xunit;
namespace DotNet.ReleasesTests;
public class CoreReleaseIndexServiceTests
public sealed class CoreReleaseIndexServiceTests
{
readonly MemoryCache _cache = new(Options.Create(new MemoryCacheOptions()));

Просмотреть файл

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">

Просмотреть файл

@ -8,7 +8,7 @@ using Xunit;
namespace DotNet.ReleasesTests;
public class FrameworkReleaseServiceTests
public sealed class FrameworkReleaseServiceTests
{
readonly IFrameworkReleaseIndexService _indexService = new FrameworkReleaseIndexService();
readonly MemoryCache _cache = new(Options.Create(new MemoryCacheOptions()));

Просмотреть файл

@ -6,7 +6,7 @@ using Xunit;
namespace DotNet.ReleasesTests;
public class LabeledVersionTests
public sealed class LabeledVersionTests
{
public static IEnumerable<object[]> AsLabeledVersionInput = new[]
{

Просмотреть файл

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">

Просмотреть файл

@ -6,7 +6,7 @@ using Xunit;
namespace DotNet.VersionSweeperTests;
public class OptionsTests
public sealed class OptionsTests
{
public static IEnumerable<object[]> NewOptionsSearchPatternInput = new[]
{

Просмотреть файл

@ -6,7 +6,7 @@ using Xunit;
namespace DotNet.VersionSweeperTests;
public class StringExtensionsTests
public sealed class StringExtensionsTests
{
public static IEnumerable<object[]> AsMaskedExtensionsInput = new[]
{