1+ namespace Digdir . Bod . RoadmapReport ;
2+
3+ using System ;
4+ using System . Collections . Generic ;
5+ using System . IO ;
6+ using System . Net . Http ;
7+ using System . Net . Http . Headers ;
8+ using System . Text . Json ;
9+ using System . Text . Json . Serialization ;
10+ using System . Threading ;
11+ using System . Threading . Tasks ;
12+ using Microsoft . Extensions . Configuration ;
13+
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+
34+ public class GitHubIssueCache
35+ {
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
38+ private static readonly string CacheDirectory = Path . Combine ( Environment . GetEnvironmentVariable ( "HOME" ) ?? AppContext . BaseDirectory , "cache" ) ;
39+ private static readonly string CacheFilePath = Path . Combine ( CacheDirectory , "GitHubIssuesCache.json" ) ;
40+ private static readonly TimeSpan CacheDuration = TimeSpan . FromHours ( 1 ) ;
41+
42+ private readonly IConfiguration _configuration ;
43+ private static readonly SemaphoreSlim CacheLock = new ( 1 , 1 ) ;
44+
45+ public GitHubIssueCache ( IConfiguration configuration )
46+ {
47+ _configuration = configuration ;
48+ }
49+
50+ public async Task < List < GitHubIssue > > GetIssuesWithCustomPropertiesAsync ( )
51+ {
52+ await CacheLock . WaitAsync ( ) ;
53+ try
54+ {
55+ Directory . CreateDirectory ( CacheDirectory ) ;
56+
57+ if ( File . Exists ( CacheFilePath ) )
58+ {
59+ var fileInfo = new FileInfo ( CacheFilePath ) ;
60+ if ( DateTime . UtcNow - fileInfo . LastWriteTimeUtc < CacheDuration )
61+ {
62+ var cachedData = await File . ReadAllTextAsync ( CacheFilePath ) ;
63+ return JsonSerializer . Deserialize < List < GitHubIssue > > ( cachedData ) ?? new List < GitHubIssue > ( ) ;
64+ }
65+ }
66+
67+ var token = _configuration [ "GitHubToken" ] ;
68+ if ( string . IsNullOrEmpty ( token ) )
69+ {
70+ throw new InvalidOperationException ( "GitHub token is not configured." ) ;
71+ }
72+
73+ using var httpClient = new HttpClient ( ) ;
74+ httpClient . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Bearer" , token ) ;
75+ httpClient . DefaultRequestHeaders . UserAgent . ParseAdd ( "github.com_digdir_roadmap-report" ) ;
76+
77+ var issues = new List < GitHubIssue > ( ) ;
78+ string ? cursor = null ;
79+ bool hasMore = true ;
80+
81+ while ( hasMore )
82+ {
83+ var query = new
84+ {
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+ }
106+ }
107+ fieldValues(first: 100) {
108+ nodes {
109+ ... on ProjectV2ItemFieldTextValue {
110+ text
111+ field {
112+ ... on ProjectV2FieldCommon {
113+ name
114+ }
115+ }
116+ }
117+ ... on ProjectV2ItemFieldSingleSelectValue {
118+ name
119+ optionId
120+ field {
121+ ... on ProjectV2FieldCommon {
122+ name
123+ }
124+ }
125+ }
126+ ... on ProjectV2ItemFieldNumberValue {
127+ number
128+ field {
129+ ... on ProjectV2FieldCommon {
130+ name
131+ }
132+ }
133+ }
134+ ... on ProjectV2ItemFieldDateValue {
135+ date
136+ field {
137+ ... on ProjectV2FieldCommon {
138+ name
139+ }
140+ }
141+ }
142+ }
143+ }
144+ }
145+ }
146+ }
147+ }
148+ }
149+ }" ,
150+ variables = new { projectId = ProjectId , cursor }
151+ } ;
152+
153+ var jsonRequest = JsonSerializer . Serialize ( query ) ;
154+ var httpContent = new StringContent ( jsonRequest ) ;
155+ httpContent . Headers . ContentType = new MediaTypeHeaderValue ( "application/json" ) ;
156+
157+ var response = await httpClient . PostAsync ( GraphQlEndpoint , httpContent ) ;
158+
159+ if ( ! response . IsSuccessStatusCode )
160+ {
161+ throw new InvalidOperationException ( $ "Failed to fetch data: { response . StatusCode } ") ;
162+ }
163+
164+ var responseContent = await response . Content . ReadAsStringAsync ( ) ;
165+ var data = JsonSerializer . Deserialize < GraphQLResponse > ( responseContent , new JsonSerializerOptions
166+ {
167+ PropertyNameCaseInsensitive = true
168+ } ) ;
169+
170+ var items = data ? . Data . Node . Items ;
171+ if ( items == null ) break ;
172+
173+ foreach ( var edge in items . Edges )
174+ {
175+ var content = edge . Node . Content ;
176+ var fieldValues = edge . Node . FieldValues . Nodes ;
177+
178+ if ( ! content . Labels . Nodes . Exists ( x => x . Name == "program/nye-altinn" ) ) continue ;
179+
180+ var issue = new GitHubIssue
181+ {
182+ Number = content . Number ,
183+ Title = content . Title ,
184+ ClosedAt = content . ClosedAt ,
185+ Labels = content . Labels . Nodes ,
186+ CustomProperties = new List < GitHubCustomProperty > ( )
187+ } ;
188+
189+ foreach ( var fieldValue in fieldValues )
190+ {
191+ if ( fieldValue . Field == null ) continue ;
192+ //if (fieldValue.Date == null) continue;
193+ issue . CustomProperties . Add ( new GitHubCustomProperty
194+ {
195+ Name = fieldValue . Field . Name ,
196+ Value = fieldValue . Text ?? fieldValue . Name ?? fieldValue . Number ? . ToString ( ) ?? fieldValue . Date ?? string . Empty
197+ } ) ;
198+ }
199+
200+ issues . Add ( issue ) ;
201+ }
202+
203+ hasMore = items . PageInfo . HasNextPage ;
204+ cursor = items . PageInfo . EndCursor ;
205+ }
206+
207+ var serializedData = JsonSerializer . Serialize ( issues , new JsonSerializerOptions { WriteIndented = true } ) ;
208+ await File . WriteAllTextAsync ( CacheFilePath , serializedData ) ;
209+
210+ return issues ;
211+ }
212+ finally
213+ {
214+ CacheLock . Release ( ) ;
215+ }
216+ }
217+ }
218+
219+ public class GraphQLResponse
220+ {
221+ [ JsonPropertyName ( "data" ) ]
222+ public GraphQLData Data { get ; init ; } = null ! ;
223+ }
224+
225+ public class GraphQLData
226+ {
227+ [ JsonPropertyName ( "node" ) ]
228+ public GraphQLNode Node { get ; init ; } = null ! ;
229+ }
230+
231+ public class GraphQLNode
232+ {
233+ [ JsonPropertyName ( "items" ) ]
234+ public GraphQLItems Items { get ; init ; } = null ! ;
235+ }
236+
237+ public class GraphQLItems
238+ {
239+ [ JsonPropertyName ( "pageInfo" ) ]
240+ public GraphQLPageInfo PageInfo { get ; init ; } = null ! ;
241+
242+ [ JsonPropertyName ( "edges" ) ]
243+ public List < GraphQLEdge > Edges { get ; init ; } = new ( ) ;
244+ }
245+
246+ public class GraphQLEdge
247+ {
248+ [ JsonPropertyName ( "node" ) ]
249+ public GraphQLItemNode Node { get ; init ; } = null ! ;
250+ }
251+
252+ public class GraphQLItemNode
253+ {
254+ [ JsonPropertyName ( "content" ) ]
255+ public GraphQLContent Content { get ; init ; } = null ! ;
256+
257+ [ JsonPropertyName ( "fieldValues" ) ]
258+ public GraphQLFieldValues FieldValues { get ; init ; } = null ! ;
259+ }
260+
261+ public class GraphQLContent
262+ {
263+ [ JsonPropertyName ( "number" ) ]
264+ public int Number { get ; init ; }
265+
266+ [ JsonPropertyName ( "title" ) ]
267+ public string Title { get ; init ; } = null ! ;
268+
269+ [ JsonPropertyName ( "closedAt" ) ]
270+ public DateTimeOffset ? ClosedAt { get ; init ; } = null ;
271+
272+ [ JsonPropertyName ( "labels" ) ]
273+ public GraphQLLabels Labels { get ; init ; } = new ( ) ;
274+ }
275+
276+ public class GraphQLLabels
277+ {
278+ [ JsonPropertyName ( "nodes" ) ]
279+ public List < GitHubLabel > Nodes { get ; init ; } = new ( ) ;
280+ }
281+
282+ public class GraphQLFieldValues
283+ {
284+ [ JsonPropertyName ( "nodes" ) ]
285+ public List < GraphQLFieldValue > Nodes { get ; init ; } = new ( ) ;
286+ }
287+
288+ public class GraphQLFieldValue
289+ {
290+ [ JsonPropertyName ( "field" ) ]
291+ public GraphQLField Field { get ; init ; } = null ! ;
292+
293+ [ JsonPropertyName ( "text" ) ]
294+ public string ? Text { get ; init ; }
295+
296+ [ JsonPropertyName ( "name" ) ]
297+ public string ? Name { get ; init ; }
298+
299+ [ JsonPropertyName ( "number" ) ]
300+ public double ? Number { get ; init ; }
301+
302+ [ JsonPropertyName ( "date" ) ]
303+ public string ? Date { get ; init ; }
304+ }
305+
306+ public class GraphQLField
307+ {
308+ [ JsonPropertyName ( "name" ) ]
309+ public string Name { get ; init ; } = null ! ;
310+ }
311+
312+ public class GraphQLPageInfo
313+ {
314+ [ JsonPropertyName ( "hasNextPage" ) ]
315+ public bool HasNextPage { get ; init ; }
316+
317+ [ JsonPropertyName ( "endCursor" ) ]
318+ public string ? EndCursor { get ; init ; }
319+ }
0 commit comments