Skip to content

Commit b24d2c9

Browse files
authored
chore: Dotty refactor (#2618)
1 parent 82f601d commit b24d2c9

File tree

4 files changed

+223
-157
lines changed

4 files changed

+223
-157
lines changed

.github/workflows/nuget_slack_notifications.yml

+24-46
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ name: Check for new core technologies
22

33
on:
44
schedule:
5-
- cron: '0 10 * * *'
5+
- cron: '0 10 * * 1' # Every Monday at 10:00 AM
66
workflow_dispatch:
77
inputs:
88
daysToSearch:
9-
description: "Days of NuGet history to search for package updates"
10-
default: "1"
9+
description: "Days of NuGet history to search for package updates (0 to search since last run of this workflow on main)"
10+
default: "0"
1111
type: string
1212
testMode:
1313
description: "If checked, no notification message will be sent to the team channel, nor will any Github issues be created."
@@ -49,7 +49,27 @@ jobs:
4949
dotnet add nugetSlackNotifications.csproj package NewRelic.Agent
5050
dotnet publish -o ${{ env.scan-tool-publish-path }}
5151
52+
- name: Find timestamp of most recent run of this workflow and set as an environment variable
53+
if: inputs.daysToSearch == '0' || github.event_name == 'schedule'
54+
env:
55+
GH_TOKEN: ${{ github.token }}
56+
run: |
57+
echo "LAST_RUN_TIMESTAMP=$(gh run list --workflow nuget_slack_notifications.yml --branch main --status completed --limit 1 --json updatedAt | jq -r '.[0].updatedAt')" >> $GITHUB_ENV
58+
shell: bash
59+
5260
- name: Check for updates to core technology packages
61+
env:
62+
DOTTY_WEBHOOK: ${{ secrets.SLACK_NUGET_NOTIFICATIONS_WEBHOOK }}
63+
DOTTY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
64+
CORECLR_ENABLE_PROFILING: 1
65+
CORECLR_NEWRELIC_HOME: ${{ env.scan-tool-publish-path }}/newrelic
66+
CORECLR_PROFILER: "{36032161-FFC0-4B61-B559-F6C5D41BAE5A}"
67+
CORECLR_PROFILER_PATH: ${{ env.scan-tool-publish-path }}/newrelic/libNewRelicProfiler.so
68+
NEW_RELIC_APP_NAME: Dotty
69+
NEW_RELIC_HOST: staging-collector.newrelic.com
70+
NEW_RELIC_LICENSE_KEY: ${{ secrets.STAGING_LICENSE_KEY }}
71+
DOTTY_LAST_RUN_TIMESTAMP: ${{ env.LAST_RUN_TIMESTAMP }}
72+
5373
run: |
5474
if [ ${{ inputs.daysToSearch }} != "" ]; then
5575
export DOTTY_DAYS_TO_SEARCH=${{ inputs.daysToSearch }}
@@ -60,46 +80,4 @@ jobs:
6080
cd ${{ env.scan-tool-publish-path }}
6181
dotnet ./nugetSlackNotifications.dll ${{ env.nugets }}
6282
shell: bash
63-
64-
env:
65-
DOTTY_WEBHOOK: ${{ secrets.SLACK_NUGET_NOTIFICATIONS_WEBHOOK }}
66-
DOTTY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
67-
CORECLR_ENABLE_PROFILING: 1
68-
CORECLR_NEWRELIC_HOME: ${{ env.scan-tool-publish-path }}/newrelic
69-
CORECLR_PROFILER: "{36032161-FFC0-4B61-B559-F6C5D41BAE5A}"
70-
CORECLR_PROFILER_PATH: ${{ env.scan-tool-publish-path }}/newrelic/libNewRelicProfiler.so
71-
NEW_RELIC_APP_NAME: Dotty
72-
NEW_RELIC_HOST: staging-collector.newrelic.com
73-
NEW_RELIC_LICENSE_KEY: ${{ secrets.STAGING_LICENSE_KEY }}
74-
nugets:
75-
"amazon.lambda.apigatewayevents
76-
amazon.lambda.applicationloadbalancerevents
77-
amazon.lambda.cloudwatchevents
78-
amazon.lambda.dynamodbevents
79-
amazon.lambda.kinesisevents
80-
amazon.lambda.kinesisfirehoseevents
81-
amazon.lambda.s3events
82-
amazon.lambda.simpleemailevents
83-
amazon.lambda.snsevents
84-
amazon.lambda.sqsevents
85-
elasticsearch.net
86-
elastic.clients.elasticsearch
87-
log4net
88-
microsoft.extensions.logging
89-
microsoft.data.sqlclient
90-
microsoft.net.http
91-
mongodb.driver
92-
mysql.data
93-
mysqlconnector
94-
nest
95-
nlog
96-
rabbitmq.client
97-
restsharp
98-
serilog
99-
serilog.extensions.logging
100-
serilog.aspnetcore
101-
serilog.sinks.file
102-
serilog.sinks.console
103-
stackexchange.redis
104-
system.data.sqlclient"
105-
83+

.github/workflows/scripts/nugetSlackNotifications/Program.cs

+100-110
Original file line numberDiff line numberDiff line change
@@ -3,85 +3,133 @@
33
using Serilog;
44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Net;
78
using System.Net.Http;
89
using System.Text;
910
using System.Text.Json;
1011
using System.Text.Json.Serialization;
1112
using System.Threading.Tasks;
12-
13-
#pragma warning disable CS8618
13+
using NuGet.Common;
14+
using NuGet.Configuration;
15+
using NuGet.Protocol;
16+
using NuGet.Protocol.Core.Types;
17+
using Repository = NuGet.Protocol.Core.Types.Repository;
1418

1519
namespace nugetSlackNotifications
1620
{
1721
public class Program
1822
{
19-
// the semver2 registration endpoint returns gzip encoded json
20-
private static readonly HttpClient _client = new(new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate });
23+
private static readonly HttpClient _client = new();
2124
private static List<NugetVersionData> _newVersions = new();
2225

23-
private static readonly int _daysToSearch = int.TryParse(Environment.GetEnvironmentVariable("DOTTY_DAYS_TO_SEARCH"), out var days) ? days : 1; // How many days of package release history to scan for changes
26+
private static readonly int _daysToSearch = int.TryParse(Environment.GetEnvironmentVariable("DOTTY_DAYS_TO_SEARCH"), out var days) ? int.Max(1, days) : 1; // How many days of package release history to scan for changes
2427
private static readonly bool _testMode = bool.TryParse(Environment.GetEnvironmentVariable("DOTTY_TEST_MODE"), out var testMode) ? testMode : false;
25-
private static readonly string? _webhook = Environment.GetEnvironmentVariable("DOTTY_WEBHOOK");
26-
private static readonly string? _githubToken = Environment.GetEnvironmentVariable("DOTTY_TOKEN");
28+
private static readonly string _webhook = Environment.GetEnvironmentVariable("DOTTY_WEBHOOK");
29+
private static readonly string _githubToken = Environment.GetEnvironmentVariable("DOTTY_TOKEN");
30+
private static readonly DateTimeOffset _lastRunTimestamp = DateTimeOffset.TryParse(Environment.GetEnvironmentVariable("DOTTY_LAST_RUN_TIMESTAMP"), out var timestamp) ? timestamp : DateTimeOffset.MinValue;
31+
private const string PackageInfoFilename = "packageInfo.json";
2732

2833

29-
static async Task Main(string[] args)
34+
static async Task Main()
3035
{
3136
Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger();
3237

33-
foreach (string packageName in args)
38+
// searchTime is the date to search for package updates from.
39+
// If _lastRunTimestamp is not set, search from _daysToSearch days ago.
40+
// Otherwise, search from _lastRunTimestamp.
41+
var searchTime = _lastRunTimestamp == DateTimeOffset.MinValue ? DateTimeOffset.UtcNow.Date.AddDays(-_daysToSearch) : _lastRunTimestamp;
42+
43+
Log.Information($"Searching for package updates since {searchTime.ToUniversalTime():s}Z.");
44+
45+
// initialize nuget repo
46+
var ps = new PackageSource("https://api.nuget.org/v3/index.json");
47+
var sourceRepository = Repository.Factory.GetCoreV3(ps);
48+
var metadataResource = await sourceRepository.GetResourceAsync<PackageMetadataResource>();
49+
var sourceCacheContext = new SourceCacheContext();
50+
51+
if (!System.IO.File.Exists(PackageInfoFilename))
52+
{
53+
Log.Error($"{PackageInfoFilename} not found in the current directory. Exiting.");
54+
return;
55+
}
56+
57+
var packageInfoJson = await System.IO.File.ReadAllTextAsync(PackageInfoFilename);
58+
var packageInfos = JsonSerializer.Deserialize<PackageInfo[]>(packageInfoJson);
59+
60+
foreach (var package in packageInfos)
3461
{
3562
try
3663
{
37-
await CheckPackage(packageName);
64+
await CheckPackage(package, metadataResource, sourceCacheContext, searchTime);
3865
}
3966
catch (Exception ex)
4067
{
41-
Log.Error(ex, $"Caught exception while checking {packageName} for updates.");
42-
await SendSlackNotification($"Dotty: caught exception while checking {packageName} for updates: {ex}");
68+
Log.Error(ex, $"Caught exception while checking {package.PackageName} for updates.");
69+
await SendSlackNotification($"Dotty: caught exception while checking {package.PackageName} for updates: {ex}");
4370
}
4471
}
4572

4673
await AlertOnNewVersions();
4774
await CreateGithubIssuesForNewVersions();
48-
4975
}
5076

5177
[Transaction]
52-
static async Task CheckPackage(string packageName)
78+
static async Task CheckPackage(PackageInfo package, PackageMetadataResource metadataResource, SourceCacheContext sourceCacheContext, DateTimeOffset searchTime)
5379
{
54-
var response = await _client.GetStringAsync($"https://api.nuget.org/v3/registration5-gz-semver2/{packageName}/index.json");
80+
var packageName = package.PackageName;
81+
82+
var metaData = (await metadataResource.GetMetadataAsync(packageName, false, false, sourceCacheContext, NullLogger.Instance, System.Threading.CancellationToken.None)).OrderByDescending(p => p.Identity.Version).ToList();
5583

56-
SearchResult? searchResult = JsonSerializer.Deserialize<SearchResult>(response);
57-
if (searchResult is null)
84+
if (!metaData.Any())
5885
{
59-
Log.Warning($"CheckPackage: null search result for package {packageName}");
86+
Log.Warning($"CheckPackage: No metadata found for package {packageName}");
6087
return;
6188
}
6289

63-
Item item = searchResult.items[^1]; // get the most recent group
90+
// get the most recent version of the package
91+
var latest = metaData.First();
92+
packageName = latest.Identity.Id;
6493

65-
// need to make another request to get the page with individual release listings
66-
Page? page = JsonSerializer.Deserialize<Page>(await _client.GetStringAsync(item.id));
67-
if (page is null)
94+
// get the second most recent version of the package (if there is one)
95+
var previous = metaData.Skip(1).FirstOrDefault();
96+
97+
// check publish date
98+
if (latest.Published >= searchTime)
6899
{
69-
Log.Warning($"CheckPackage: null page result for package {packageName}, item id {item.id}");
70-
return;
71-
}
100+
if (previous != null && (package.IgnorePatch || package.IgnoreMinor))
101+
{
102+
var previousVersion = previous.Identity.Version;
103+
var latestVersion = latest.Identity.Version;
72104

73-
// need to get the most recent and previous catalog entries to display previous and new version
74-
Catalogentry? latestCatalogEntry = GetCatalogentry(packageName, page, 1);
75-
Catalogentry? previousCatalogEntry = GetCatalogentry(packageName, page, 2);
105+
if (package.IgnorePatch)
106+
{
107+
if (previousVersion.Major == latestVersion.Major && previousVersion.Minor == latestVersion.Minor)
108+
{
109+
Log.Information($"Package {packageName} ignores Patch version updates; the Minor version ({latestVersion.Major}.{latestVersion.Minor:2}) has not been updated.");
110+
return;
111+
}
112+
}
113+
114+
if (package.IgnoreMinor)
115+
{
76116

77-
if (latestCatalogEntry?.published > DateTime.Now.AddDays(-_daysToSearch) && !await latestCatalogEntry.isPrerelease())
78-
{
79-
Log.Information($"Package {packageName} has been updated in the past {_daysToSearch} days.");
80-
_newVersions.Add(new NugetVersionData(packageName, previousCatalogEntry?.version ?? "Unknown", latestCatalogEntry.version, $"https://www.nuget.org/packages/{packageName}/"));
117+
if (previousVersion.Major == latestVersion.Major)
118+
{
119+
Log.Information($"Package {packageName} ignores Minor version updates; the Major version ({latestVersion.Major}) has not been updated.");
120+
return;
121+
}
122+
}
123+
}
124+
125+
var previousVersionDescription = previous?.Identity.Version.ToNormalizedString() ?? "Unknown";
126+
var latestVersionDescription = latest.Identity.Version.ToNormalizedString();
127+
Log.Information($"Package {packageName} was updated from {previousVersionDescription} to {latestVersionDescription}.");
128+
_newVersions.Add(new NugetVersionData(packageName, previousVersionDescription, latestVersionDescription, latest.PackageDetailsUrl.ToString(), latest.Published.Value.Date));
81129
}
82130
else
83131
{
84-
Log.Information($"Package {packageName} has not been updated in the past {_daysToSearch} days.");
132+
Log.Information($"Package {packageName} has NOT been updated.");
85133
}
86134
}
87135

@@ -91,10 +139,10 @@ static async Task AlertOnNewVersions()
91139

92140
if (_newVersions.Count > 0 && _webhook != null && !_testMode) // only message channel if there's package updates to report AND we have a webhook from the environment AND we're not in test mode
93141
{
94-
string msg = "Hi team! Dotty here :technologist::pager:\nThere's some new NuGet releases you should know about :arrow_heading_down::sparkles:";
142+
var msg = "Hi team! Dotty here :technologist::pager:\nThere's some new NuGet releases you should know about :arrow_heading_down::sparkles:";
95143
foreach (var versionData in _newVersions)
96144
{
97-
msg += $"\n\t:package: {char.ToUpper(versionData.PackageName[0]) + versionData.PackageName[1..]} {versionData.OldVersion} :point_right: <{versionData.Url}|{versionData.NewVersion}>";
145+
msg += $"\n\t:package: {versionData.PackageName} {versionData.OldVersion} :point_right: <{versionData.Url}|{versionData.NewVersion}>";
98146
}
99147
msg += $"\nThanks and have a wonderful {DateTime.Now.DayOfWeek}.";
100148

@@ -117,11 +165,14 @@ static async Task CreateGithubIssuesForNewVersions()
117165
ghClient.Credentials = tokenAuth;
118166
foreach (var versionData in _newVersions)
119167
{
120-
var newIssue = new NewIssue($"Dotty: update tests for {versionData.PackageName} from {versionData.OldVersion} to {versionData.NewVersion}");
121-
newIssue.Body = versionData.Url;
168+
var newIssue = new NewIssue($"Dotty: update tests for {versionData.PackageName} from {versionData.OldVersion} to {versionData.NewVersion}")
169+
{
170+
Body = $"Package [{versionData.PackageName}]({versionData.Url}) was updated from {versionData.OldVersion} to {versionData.NewVersion} on {versionData.PublishDate.ToShortDateString()}."
171+
};
122172
newIssue.Labels.Add("testing");
123173
newIssue.Labels.Add("Core Technologies");
124174
var issue = await ghClient.Issue.Create("newrelic", "newrelic-dotnet-agent", newIssue);
175+
Log.Information($"Created issue #{issue.Id} for {versionData.PackageName} update to {versionData.NewVersion} in newrelic/newrelic-dotnet-agent.");
125176
}
126177
}
127178
else
@@ -160,29 +211,6 @@ static async Task SendSlackNotification(string msg)
160211
Log.Error($"SendSlackNotification called but _webhook is null. msg={msg}");
161212
}
162213
}
163-
164-
[Trace]
165-
static Catalogentry? GetCatalogentry(string packageName, Page page, int releaseIndex)
166-
{
167-
// release index = 1 for most recent release, 2 for the previous release, etc.
168-
try
169-
{
170-
// alternative json structure (see mysql.data)
171-
if (page.items[^1].catalogEntry is null)
172-
{
173-
return page.items[^1].items[^releaseIndex].catalogEntry; // latest release
174-
}
175-
else // standard structure
176-
{
177-
return page.items[^releaseIndex].catalogEntry;
178-
}
179-
}
180-
catch (IndexOutOfRangeException)
181-
{
182-
Log.Warning($"GetCatalogEntry: array index issue for package {packageName} and releaseIndex {releaseIndex}");
183-
return null;
184-
}
185-
}
186214
}
187215

188216
public class NugetVersionData
@@ -191,65 +219,27 @@ public class NugetVersionData
191219
public string OldVersion { get; set; }
192220
public string NewVersion { get; set; }
193221
public string Url { get; set; }
222+
public DateTime PublishDate { get; set; }
194223

195-
public NugetVersionData(string packageName, string oldVersion, string newVersion, string url)
224+
public NugetVersionData(string packageName, string oldVersion, string newVersion, string url, DateTime publishDate)
196225
{
197226
PackageName = packageName;
198227
OldVersion = oldVersion;
199228
NewVersion = newVersion;
200229
Url = url;
230+
PublishDate = publishDate;
201231
}
202232
}
203233

204-
public class SearchResult
205-
{
206-
public int count { get; set; }
207-
public Item[] items { get; set; }
208-
}
209-
210-
public class Item
211-
{
212-
[JsonPropertyName("@id")]
213-
public string id { get; set; }
214-
public DateTime commitTimeStamp { get; set; }
215-
}
216-
217-
public class Page
218-
{
219-
public Release[] items { get; set; }
220-
}
221-
222-
public class Release
234+
public class PackageInfo
223235
{
224-
[JsonPropertyName("@id")]
225-
public string id { get; set; }
226-
public Catalogentry catalogEntry { get; set; }
227-
228-
// only packages with alternative json structure (i.e. mysql.data) will have this
229-
public Release[] items { get; set; }
230-
}
231-
232-
public class Version
233-
{
234-
public bool isPrerelease { get; set; }
235-
}
236-
237-
public class Catalogentry
238-
{
239-
[JsonPropertyName("@id")]
240-
public string id { get; set; }
241-
public DateTime published { get; set; }
242-
public string version { get; set; }
243-
244-
public async Task<bool> isPrerelease()
245-
{
246-
HttpClient client = new();
247-
string response = await client.GetStringAsync(id);
248-
Version? version = JsonSerializer.Deserialize<Version>(response);
249-
250-
return version is null ? false : version.isPrerelease;
251-
}
236+
[JsonPropertyName("packageName")]
237+
public string PackageName { get; set; }
238+
[JsonPropertyName("ignorePatch")]
239+
public bool IgnorePatch { get; set; }
240+
[JsonPropertyName("ignoreMinor")]
241+
public bool IgnoreMinor { get; set; }
242+
[JsonPropertyName("ignoreReason")]
243+
public string IgnoreReason {get; set;}
252244
}
253245
}
254-
255-
#pragma warning restore CS8618

0 commit comments

Comments
 (0)