@@ -11,40 +11,25 @@ namespace Digdir.Bod.RoadmapReport;
1111using System . Threading . Tasks ;
1212using Microsoft . Extensions . Configuration ;
1313
14- public class GitHubIssue
15- {
16- public int Number { get ; init ; }
17- public string Title { get ; init ; } = null ! ;
18- public DateTimeOffset ? ClosedAt { get ; init ; }
19- public List < GitHubLabel > Labels { get ; init ; } = new ( ) ;
20- public List < GitHubCustomProperty > CustomProperties { get ; init ; } = new ( ) ;
21- }
22-
23- public class GitHubLabel
24- {
25- public string Name { get ; init ; } = null ! ;
26- }
27-
28- public class GitHubCustomProperty
29- {
30- public string Name { get ; init ; } = null ! ;
31- public string Value { get ; init ; } = null ! ;
32- }
33-
3414public class GitHubIssueCache
3515{
36- private const string GraphQlEndpoint = "https://api.github.com/graphql" ;
37- private const string ProjectId = "PVT_kwDOAwyZKM4ANYNj" ; // Replace with the actual project ID from GitHub
16+ private const string GqlEndpoint = "https://api.github.com/graphql" ;
3817 private static readonly string CacheDirectory = Path . Combine ( Environment . GetEnvironmentVariable ( "HOME" ) ?? AppContext . BaseDirectory , "cache" ) ;
3918 private static readonly string CacheFilePath = Path . Combine ( CacheDirectory , "GitHubIssuesCache.json" ) ;
40- private static readonly TimeSpan CacheDuration = TimeSpan . FromHours ( 1 ) ;
4119
4220 private readonly IConfiguration _configuration ;
21+ private readonly IHttpClientFactory _clientFactory ;
4322 private static readonly SemaphoreSlim CacheLock = new ( 1 , 1 ) ;
23+ private static readonly JsonSerializerOptions JsonSerializerOptions = new ( )
24+ {
25+ PropertyNameCaseInsensitive = true ,
26+ WriteIndented = true
27+ } ;
4428
45- public GitHubIssueCache ( IConfiguration configuration )
29+ public GitHubIssueCache ( IConfiguration configuration , IHttpClientFactory clientFactory )
4630 {
4731 _configuration = configuration ;
32+ _clientFactory = clientFactory ;
4833 }
4934
5035 public async Task < List < GitHubIssue > > GetIssuesWithCustomPropertiesAsync ( )
@@ -57,10 +42,10 @@ public async Task<List<GitHubIssue>> GetIssuesWithCustomPropertiesAsync()
5742 if ( File . Exists ( CacheFilePath ) )
5843 {
5944 var fileInfo = new FileInfo ( CacheFilePath ) ;
60- if ( DateTime . UtcNow - fileInfo . LastWriteTimeUtc < CacheDuration )
45+ if ( DateTime . UtcNow - fileInfo . LastWriteTimeUtc < TimeSpan . Parse ( _configuration [ "GitHubCacheDuration" ] ?? "01:00:00" ) )
6146 {
6247 var cachedData = await File . ReadAllTextAsync ( CacheFilePath ) ;
63- return JsonSerializer . Deserialize < List < GitHubIssue > > ( cachedData ) ?? new List < GitHubIssue > ( ) ;
48+ return JsonSerializer . Deserialize < List < GitHubIssue > > ( cachedData ) ?? [ ] ;
6449 }
6550 }
6651
@@ -70,72 +55,75 @@ public async Task<List<GitHubIssue>> GetIssuesWithCustomPropertiesAsync()
7055 throw new InvalidOperationException ( "GitHub token is not configured." ) ;
7156 }
7257
73- using var httpClient = new HttpClient ( ) ;
58+ var httpClient = _clientFactory . CreateClient ( ) ;
7459 httpClient . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Bearer" , token ) ;
7560 httpClient . DefaultRequestHeaders . UserAgent . ParseAdd ( "github.com_digdir_roadmap-report" ) ;
7661
7762 var issues = new List < GitHubIssue > ( ) ;
7863 string ? cursor = null ;
79- bool hasMore = true ;
64+ var hasMore = true ;
8065
8166 while ( hasMore )
8267 {
8368 var query = new
8469 {
85- query = @"query ListConnectedIssuesWithLabel($projectId: ID!, $cursor: String) {
86- node(id: $projectId) {
87- ... on ProjectV2 {
88- items(first: 100, after: $cursor) {
89- pageInfo {
90- hasNextPage
91- endCursor
92- }
93- edges {
94- node {
95- content {
96- ... on Issue {
97- number
98- title
99- closedAt
100- labels(first: 10) {
101- nodes {
102- name
103- }
104- }
105- }
70+ query = """
71+ query ListConnectedIssuesWithLabel($projectId: ID!, $cursor: String) {
72+ node(id: $projectId) {
73+ ... on ProjectV2 {
74+ items(first: 100, after: $cursor) {
75+ pageInfo {
76+ hasNextPage
77+ endCursor
10678 }
107- fieldValues(first: 100) {
108- nodes {
109- ... on ProjectV2ItemFieldTextValue {
110- text
111- field {
112- ... on ProjectV2FieldCommon {
113- name
79+ edges {
80+ node {
81+ content {
82+ ... on Issue {
83+ number
84+ title
85+ closedAt
86+ labels(first: 10) {
87+ nodes {
88+ name
89+ }
11490 }
11591 }
11692 }
117- ... on ProjectV2ItemFieldSingleSelectValue {
118- name
119- optionId
120- field {
121- ... on ProjectV2FieldCommon {
122- name
93+ fieldValues(first: 100) {
94+ nodes {
95+ ... on ProjectV2ItemFieldTextValue {
96+ text
97+ field {
98+ ... on ProjectV2FieldCommon {
99+ name
100+ }
101+ }
123102 }
124- }
125- }
126- ... on ProjectV2ItemFieldNumberValue {
127- number
128- field {
129- ... on ProjectV2FieldCommon {
103+ ... on ProjectV2ItemFieldSingleSelectValue {
130104 name
105+ optionId
106+ field {
107+ ... on ProjectV2FieldCommon {
108+ name
109+ }
110+ }
131111 }
132- }
133- }
134- ... on ProjectV2ItemFieldDateValue {
135- date
136- field {
137- ... on ProjectV2FieldCommon {
138- name
112+ ... on ProjectV2ItemFieldNumberValue {
113+ number
114+ field {
115+ ... on ProjectV2FieldCommon {
116+ name
117+ }
118+ }
119+ }
120+ ... on ProjectV2ItemFieldDateValue {
121+ date
122+ field {
123+ ... on ProjectV2FieldCommon {
124+ name
125+ }
126+ }
139127 }
140128 }
141129 }
@@ -145,27 +133,23 @@ ... on ProjectV2FieldCommon {
145133 }
146134 }
147135 }
148- }
149- }" ,
150- variables = new { projectId = ProjectId , cursor }
136+ """ ,
137+ variables = new { projectId = _configuration [ "GitHubProjectId" ] , cursor }
151138 } ;
152139
153140 var jsonRequest = JsonSerializer . Serialize ( query ) ;
154141 var httpContent = new StringContent ( jsonRequest ) ;
155142 httpContent . Headers . ContentType = new MediaTypeHeaderValue ( "application/json" ) ;
156143
157- var response = await httpClient . PostAsync ( GraphQlEndpoint , httpContent ) ;
144+ var response = await httpClient . PostAsync ( GqlEndpoint , httpContent ) ;
158145
159146 if ( ! response . IsSuccessStatusCode )
160147 {
161148 throw new InvalidOperationException ( $ "Failed to fetch data: { response . StatusCode } ") ;
162149 }
163150
164151 var responseContent = await response . Content . ReadAsStringAsync ( ) ;
165- var data = JsonSerializer . Deserialize < GraphQLResponse > ( responseContent , new JsonSerializerOptions
166- {
167- PropertyNameCaseInsensitive = true
168- } ) ;
152+ var data = JsonSerializer . Deserialize < GqlResponse > ( responseContent , JsonSerializerOptions ) ;
169153
170154 var items = data ? . Data . Node . Items ;
171155 if ( items == null ) break ;
@@ -174,22 +158,19 @@ ... on ProjectV2FieldCommon {
174158 {
175159 var content = edge . Node . Content ;
176160 var fieldValues = edge . Node . FieldValues . Nodes ;
177-
178- if ( ! content . Labels . Nodes . Exists ( x => x . Name == "program/nye-altinn" ) ) continue ;
179161
180162 var issue = new GitHubIssue
181163 {
182164 Number = content . Number ,
183165 Title = content . Title ,
184166 ClosedAt = content . ClosedAt ,
185167 Labels = content . Labels . Nodes ,
186- CustomProperties = new List < GitHubCustomProperty > ( )
168+ CustomProperties = [ ]
187169 } ;
188170
189171 foreach ( var fieldValue in fieldValues )
190172 {
191173 if ( fieldValue . Field == null ) continue ;
192- //if (fieldValue.Date == null) continue;
193174 issue . CustomProperties . Add ( new GitHubCustomProperty
194175 {
195176 Name = fieldValue . Field . Name ,
@@ -204,7 +185,7 @@ ... on ProjectV2FieldCommon {
204185 cursor = items . PageInfo . EndCursor ;
205186 }
206187
207- var serializedData = JsonSerializer . Serialize ( issues , new JsonSerializerOptions { WriteIndented = true } ) ;
188+ var serializedData = JsonSerializer . Serialize ( issues , JsonSerializerOptions ) ;
208189 await File . WriteAllTextAsync ( CacheFilePath , serializedData ) ;
209190
210191 return issues ;
@@ -216,49 +197,70 @@ ... on ProjectV2FieldCommon {
216197 }
217198}
218199
219- public class GraphQLResponse
200+ public record GitHubIssue
201+ {
202+ public int Number { get ; init ; }
203+ public string Title { get ; init ; } = null ! ;
204+ public DateTimeOffset ? ClosedAt { get ; init ; }
205+ public List < GitHubLabel > Labels { get ; init ; } = [ ] ;
206+ public List < GitHubCustomProperty > CustomProperties { get ; init ; } = [ ] ;
207+ }
208+
209+ public record GitHubLabel
210+ {
211+ public string Name { get ; init ; } = null ! ;
212+ }
213+
214+ public record GitHubCustomProperty
215+ {
216+ public string Name { get ; init ; } = null ! ;
217+ public string Value { get ; init ; } = null ! ;
218+ }
219+
220+
221+ public record GqlResponse
220222{
221223 [ JsonPropertyName ( "data" ) ]
222- public GraphQLData Data { get ; init ; } = null ! ;
224+ public GqlData Data { get ; init ; } = null ! ;
223225}
224226
225- public class GraphQLData
227+ public record GqlData
226228{
227229 [ JsonPropertyName ( "node" ) ]
228- public GraphQLNode Node { get ; init ; } = null ! ;
230+ public GqlNode Node { get ; init ; } = null ! ;
229231}
230232
231- public class GraphQLNode
233+ public record GqlNode
232234{
233235 [ JsonPropertyName ( "items" ) ]
234- public GraphQLItems Items { get ; init ; } = null ! ;
236+ public GqlItems Items { get ; init ; } = null ! ;
235237}
236238
237- public class GraphQLItems
239+ public record GqlItems
238240{
239241 [ JsonPropertyName ( "pageInfo" ) ]
240- public GraphQLPageInfo PageInfo { get ; init ; } = null ! ;
242+ public GqlPageInfo PageInfo { get ; init ; } = null ! ;
241243
242244 [ JsonPropertyName ( "edges" ) ]
243- public List < GraphQLEdge > Edges { get ; init ; } = new ( ) ;
245+ public List < GqlEdge > Edges { get ; init ; } = [ ] ;
244246}
245247
246- public class GraphQLEdge
248+ public record GqlEdge
247249{
248250 [ JsonPropertyName ( "node" ) ]
249- public GraphQLItemNode Node { get ; init ; } = null ! ;
251+ public GqlItemNode Node { get ; init ; } = null ! ;
250252}
251253
252- public class GraphQLItemNode
254+ public record GqlItemNode
253255{
254256 [ JsonPropertyName ( "content" ) ]
255- public GraphQLContent Content { get ; init ; } = null ! ;
257+ public GqlContent Content { get ; init ; } = null ! ;
256258
257259 [ JsonPropertyName ( "fieldValues" ) ]
258- public GraphQLFieldValues FieldValues { get ; init ; } = null ! ;
260+ public GqlFieldValues FieldValues { get ; init ; } = null ! ;
259261}
260262
261- public class GraphQLContent
263+ public record GqlContent
262264{
263265 [ JsonPropertyName ( "number" ) ]
264266 public int Number { get ; init ; }
@@ -270,25 +272,25 @@ public class GraphQLContent
270272 public DateTimeOffset ? ClosedAt { get ; init ; } = null ;
271273
272274 [ JsonPropertyName ( "labels" ) ]
273- public GraphQLLabels Labels { get ; init ; } = new ( ) ;
275+ public GqlLabels Labels { get ; init ; } = new ( ) ;
274276}
275277
276- public class GraphQLLabels
278+ public record GqlLabels
277279{
278280 [ JsonPropertyName ( "nodes" ) ]
279- public List < GitHubLabel > Nodes { get ; init ; } = new ( ) ;
281+ public List < GitHubLabel > Nodes { get ; init ; } = [ ] ;
280282}
281283
282- public class GraphQLFieldValues
284+ public record GqlFieldValues
283285{
284286 [ JsonPropertyName ( "nodes" ) ]
285- public List < GraphQLFieldValue > Nodes { get ; init ; } = new ( ) ;
287+ public List < GqlFieldValue > Nodes { get ; init ; } = [ ] ;
286288}
287289
288- public class GraphQLFieldValue
290+ public record GqlFieldValue
289291{
290292 [ JsonPropertyName ( "field" ) ]
291- public GraphQLField Field { get ; init ; } = null ! ;
293+ public GqlField ? Field { get ; init ; } = null ! ;
292294
293295 [ JsonPropertyName ( "text" ) ]
294296 public string ? Text { get ; init ; }
@@ -303,13 +305,13 @@ public class GraphQLFieldValue
303305 public string ? Date { get ; init ; }
304306}
305307
306- public class GraphQLField
308+ public record GqlField
307309{
308310 [ JsonPropertyName ( "name" ) ]
309311 public string Name { get ; init ; } = null ! ;
310312}
311313
312- public class GraphQLPageInfo
314+ public record GqlPageInfo
313315{
314316 [ JsonPropertyName ( "hasNextPage" ) ]
315317 public bool HasNextPage { get ; init ; }
0 commit comments