Skip to content

Commit 493b46c

Browse files
committed
First version
1 parent 798fbec commit 493b46c

11 files changed

Lines changed: 598 additions & 0 deletions

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
bin/
2+
obj/
3+
/packages/
4+
riderModule.iml
5+
/_ReSharper.Caches/
6+
.idea

Digdir.Bod.RoadmapReport.sln

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Digdir.Bod.RoadmapReport", "Digdir.Bod.RoadmapReport\Digdir.Bod.RoadmapReport.csproj", "{C9B3365B-D1F6-409B-A124-309BAAA74D5F}"
4+
EndProject
5+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution files", "Solution files", "{91622E7C-E86D-4121-AB36-4601512586B1}"
6+
ProjectSection(SolutionItems) = preProject
7+
README.md = README.md
8+
EndProjectSection
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Release|Any CPU = Release|Any CPU
14+
EndGlobalSection
15+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
16+
{C9B3365B-D1F6-409B-A124-309BAAA74D5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17+
{C9B3365B-D1F6-409B-A124-309BAAA74D5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
18+
{C9B3365B-D1F6-409B-A124-309BAAA74D5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
19+
{C9B3365B-D1F6-409B-A124-309BAAA74D5F}.Release|Any CPU.Build.0 = Release|Any CPU
20+
EndGlobalSection
21+
EndGlobal
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<UserSecretsId>d6d35d64-57b9-4385-adf6-c3418734cb25</UserSecretsId>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0"/>
12+
</ItemGroup>
13+
14+
</Project>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@Digdir.Bod.RoadmapReport_HostAddress = http://localhost:5209
2+
3+
GET {{Digdir.Bod.RoadmapReport_HostAddress}}/weatherforecast/
4+
Accept: application/json
5+
6+
###
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
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

Comments
 (0)