3
3
using Serilog ;
4
4
using System ;
5
5
using System . Collections . Generic ;
6
+ using System . Linq ;
6
7
using System . Net ;
7
8
using System . Net . Http ;
8
9
using System . Text ;
9
10
using System . Text . Json ;
10
11
using System . Text . Json . Serialization ;
11
12
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 ;
14
18
15
19
namespace nugetSlackNotifications
16
20
{
17
21
public class Program
18
22
{
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 ( ) ;
21
24
private static List < NugetVersionData > _newVersions = new ( ) ;
22
25
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
24
27
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" ;
27
32
28
33
29
- static async Task Main ( string [ ] args )
34
+ static async Task Main ( )
30
35
{
31
36
Log . Logger = new LoggerConfiguration ( ) . WriteTo . Console ( ) . CreateLogger ( ) ;
32
37
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 )
34
61
{
35
62
try
36
63
{
37
- await CheckPackage ( packageName ) ;
64
+ await CheckPackage ( package , metadataResource , sourceCacheContext , searchTime ) ;
38
65
}
39
66
catch ( Exception ex )
40
67
{
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 } ") ;
43
70
}
44
71
}
45
72
46
73
await AlertOnNewVersions ( ) ;
47
74
await CreateGithubIssuesForNewVersions ( ) ;
48
-
49
75
}
50
76
51
77
[ Transaction ]
52
- static async Task CheckPackage ( string packageName )
78
+ static async Task CheckPackage ( PackageInfo package , PackageMetadataResource metadataResource , SourceCacheContext sourceCacheContext , DateTimeOffset searchTime )
53
79
{
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 ( ) ;
55
83
56
- SearchResult ? searchResult = JsonSerializer . Deserialize < SearchResult > ( response ) ;
57
- if ( searchResult is null )
84
+ if ( ! metaData . Any ( ) )
58
85
{
59
- Log . Warning ( $ "CheckPackage: null search result for package { packageName } ") ;
86
+ Log . Warning ( $ "CheckPackage: No metadata found for package { packageName } ") ;
60
87
return ;
61
88
}
62
89
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 ;
64
93
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 )
68
99
{
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 ;
72
104
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
+ {
76
116
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 ) ) ;
81
129
}
82
130
else
83
131
{
84
- Log . Information ( $ "Package { packageName } has not been updated in the past { _daysToSearch } days .") ;
132
+ Log . Information ( $ "Package { packageName } has NOT been updated.") ;
85
133
}
86
134
}
87
135
@@ -91,10 +139,10 @@ static async Task AlertOnNewVersions()
91
139
92
140
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
93
141
{
94
- string msg = "Hi team! Dotty here :technologist::pager:\n There's some new NuGet releases you should know about :arrow_heading_down::sparkles:" ;
142
+ var msg = "Hi team! Dotty here :technologist::pager:\n There's some new NuGet releases you should know about :arrow_heading_down::sparkles:" ;
95
143
foreach ( var versionData in _newVersions )
96
144
{
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 } >";
98
146
}
99
147
msg += $ "\n Thanks and have a wonderful { DateTime . Now . DayOfWeek } .";
100
148
@@ -117,11 +165,14 @@ static async Task CreateGithubIssuesForNewVersions()
117
165
ghClient . Credentials = tokenAuth ;
118
166
foreach ( var versionData in _newVersions )
119
167
{
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
+ } ;
122
172
newIssue . Labels . Add ( "testing" ) ;
123
173
newIssue . Labels . Add ( "Core Technologies" ) ;
124
174
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.") ;
125
176
}
126
177
}
127
178
else
@@ -160,29 +211,6 @@ static async Task SendSlackNotification(string msg)
160
211
Log . Error ( $ "SendSlackNotification called but _webhook is null. msg={ msg } ") ;
161
212
}
162
213
}
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
- }
186
214
}
187
215
188
216
public class NugetVersionData
@@ -191,65 +219,27 @@ public class NugetVersionData
191
219
public string OldVersion { get ; set ; }
192
220
public string NewVersion { get ; set ; }
193
221
public string Url { get ; set ; }
222
+ public DateTime PublishDate { get ; set ; }
194
223
195
- public NugetVersionData ( string packageName , string oldVersion , string newVersion , string url )
224
+ public NugetVersionData ( string packageName , string oldVersion , string newVersion , string url , DateTime publishDate )
196
225
{
197
226
PackageName = packageName ;
198
227
OldVersion = oldVersion ;
199
228
NewVersion = newVersion ;
200
229
Url = url ;
230
+ PublishDate = publishDate ;
201
231
}
202
232
}
203
233
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
223
235
{
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 ; }
252
244
}
253
245
}
254
-
255
- #pragma warning restore CS8618
0 commit comments