Add support for _Dockerfile_ (#74)
This commit is contained in:
Родитель
eeab704f6d
Коммит
623558498e
|
@ -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
|
||||
|
|
|
@ -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[]
|
||||
{
|
||||
|
|
Загрузка…
Ссылка в новой задаче