Skip to content

Commit af0f6fa

Browse files
Replace Octokit Release API with GraphQL queries using ReleaseNode (#81)
* Initial plan * Add GraphQL releases query with pagination support Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> * Use Newtonsoft.Json for Release deserialization and fix formatting Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> * Fix JSON injection vulnerability by using JsonSerializer Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> * Replace Octokit Release type with custom Release record Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> * Return ReleaseNode directly from GetReleasesAsync to eliminate extra conversions Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> * Split GitHubGraphQLClientTests into separate files by functionality Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com>
1 parent 2e1f0fb commit af0f6fa

File tree

6 files changed

+1042
-412
lines changed

6 files changed

+1042
-412
lines changed

src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLClient.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,77 @@ ... on Commit {
172172
}
173173
}
174174

175+
/// <summary>
176+
/// Gets all releases for a repository using GraphQL with pagination.
177+
/// </summary>
178+
/// <param name="owner">Repository owner.</param>
179+
/// <param name="repo">Repository name.</param>
180+
/// <returns>List of release nodes.</returns>
181+
public async Task<List<ReleaseNode>> GetReleasesAsync(
182+
string owner,
183+
string repo)
184+
{
185+
try
186+
{
187+
var allReleaseNodes = new List<ReleaseNode>();
188+
string? afterCursor = null;
189+
bool hasNextPage;
190+
191+
// Paginate through all releases
192+
do
193+
{
194+
// Create GraphQL request to get releases for a repository with pagination support
195+
var request = new GraphQLRequest
196+
{
197+
Query = @"
198+
query($owner: String!, $repo: String!, $after: String) {
199+
repository(owner: $owner, name: $repo) {
200+
releases(first: 100, after: $after, orderBy: {field: CREATED_AT, direction: DESC}) {
201+
nodes {
202+
tagName
203+
}
204+
pageInfo {
205+
hasNextPage
206+
endCursor
207+
}
208+
}
209+
}
210+
}",
211+
Variables = new
212+
{
213+
owner,
214+
repo,
215+
after = afterCursor
216+
}
217+
};
218+
219+
// Execute GraphQL query
220+
var response = await _graphqlClient.SendQueryAsync<GetReleasesResponse>(request);
221+
222+
// Extract release nodes from the GraphQL response, filtering out null or invalid values
223+
var pageReleaseNodes = response.Data?.Repository?.Releases?.Nodes?
224+
.Where(n => !string.IsNullOrEmpty(n.TagName))
225+
.ToList() ?? [];
226+
227+
allReleaseNodes.AddRange(pageReleaseNodes);
228+
229+
// Check if there are more pages
230+
var pageInfo = response.Data?.Repository?.Releases?.PageInfo;
231+
hasNextPage = pageInfo?.HasNextPage ?? false;
232+
afterCursor = pageInfo?.EndCursor;
233+
}
234+
while (hasNextPage);
235+
236+
// Return list of all release nodes
237+
return allReleaseNodes;
238+
}
239+
catch
240+
{
241+
// If GraphQL query fails, return empty list
242+
return [];
243+
}
244+
}
245+
175246
/// <summary>
176247
/// Finds issue IDs linked to a pull request via closingIssuesReferences.
177248
/// </summary>

src/DemaConsulting.BuildMark/RepoConnectors/GitHub/GitHubGraphQLTypes.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,33 @@ internal record CommitHistoryData(
109109
/// <param name="Oid">Git object ID (SHA).</param>
110110
internal record CommitNode(
111111
string? Oid);
112+
113+
/// <summary>
114+
/// Response for getting releases from a repository.
115+
/// </summary>
116+
/// <param name="Repository">Repository data containing release information.</param>
117+
internal record GetReleasesResponse(
118+
ReleaseRepositoryData? Repository);
119+
120+
/// <summary>
121+
/// Repository data containing releases information.
122+
/// </summary>
123+
/// <param name="Releases">Releases connection data.</param>
124+
internal record ReleaseRepositoryData(
125+
ReleasesConnectionData? Releases);
126+
127+
/// <summary>
128+
/// Releases connection data containing nodes and page info.
129+
/// </summary>
130+
/// <param name="Nodes">Release nodes.</param>
131+
/// <param name="PageInfo">Pagination information.</param>
132+
internal record ReleasesConnectionData(
133+
List<ReleaseNode>? Nodes,
134+
PageInfo? PageInfo);
135+
136+
/// <summary>
137+
/// Release node containing release information.
138+
/// </summary>
139+
/// <param name="TagName">Tag name associated with the release.</param>
140+
internal record ReleaseNode(
141+
string? TagName);

src/DemaConsulting.BuildMark/RepoConnectors/GitHubRepoConnector.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ internal sealed record Commit(
131131
/// </summary>
132132
internal sealed record GitHubData(
133133
IReadOnlyList<Commit> Commits,
134-
IReadOnlyList<Release> Releases,
134+
IReadOnlyList<ReleaseNode> Releases,
135135
IReadOnlyList<RepositoryTag> Tags,
136136
IReadOnlyList<PullRequest> PullRequests,
137137
IReadOnlyList<Issue> Issues);
@@ -142,9 +142,9 @@ internal sealed record GitHubData(
142142
internal sealed record LookupData(
143143
Dictionary<int, Issue> IssueById,
144144
Dictionary<string, PullRequest> CommitHashToPr,
145-
List<Release> BranchReleases,
145+
List<ReleaseNode> BranchReleases,
146146
Dictionary<string, RepositoryTag> TagsByName,
147-
Dictionary<string, Release> TagToRelease,
147+
Dictionary<string, ReleaseNode> TagToRelease,
148148
List<Version> ReleaseVersions,
149149
HashSet<string> BranchTagNames);
150150

@@ -166,7 +166,7 @@ private static async Task<GitHubData> FetchGitHubDataAsync(
166166
{
167167
// Fetch all data from GitHub in parallel
168168
var commitsTask = GetAllCommitsAsync(graphqlClient, owner, repo, branch);
169-
var releasesTask = client.Repository.Release.GetAll(owner, repo);
169+
var releasesTask = GetAllReleasesAsync(graphqlClient, owner, repo);
170170
var tagsTask = client.Repository.GetAllTags(owner, repo);
171171
var pullRequestsTask = client.PullRequest.GetAllForRepository(owner, repo, new PullRequestRequest { State = ItemStateFilter.All });
172172
var issuesTask = client.Issue.GetAllForRepository(owner, repo, new RepositoryIssueRequest { State = ItemStateFilter.All });
@@ -214,7 +214,7 @@ internal static LookupData BuildLookupData(GitHubData data)
214214
// Build an ordered list of releases on the current branch.
215215
// This is used to select the prior release version for identifying changes in the build.
216216
var branchReleases = data.Releases
217-
.Where(r => !string.IsNullOrEmpty(r.TagName) && branchTagNames.Contains(r.TagName))
217+
.Where(r => r.TagName != null && branchTagNames.Contains(r.TagName))
218218
.ToList();
219219

220220
// Build a mapping from tag name to tag object for quick lookup.
@@ -572,6 +572,22 @@ private static async Task<IReadOnlyList<Commit>> GetAllCommitsAsync(
572572
return commitShas.Select(sha => new Commit(sha)).ToList();
573573
}
574574

575+
/// <summary>
576+
/// Gets all releases for a repository using GraphQL pagination.
577+
/// </summary>
578+
/// <param name="graphqlClient">GitHub GraphQL client.</param>
579+
/// <param name="owner">Repository owner.</param>
580+
/// <param name="repo">Repository name.</param>
581+
/// <returns>List of all releases.</returns>
582+
private static async Task<IReadOnlyList<ReleaseNode>> GetAllReleasesAsync(
583+
GitHubGraphQLClient graphqlClient,
584+
string owner,
585+
string repo)
586+
{
587+
// Fetch all releases for the repository using GraphQL
588+
return await graphqlClient.GetReleasesAsync(owner, repo);
589+
}
590+
575591
/// <summary>
576592
/// Gets commits in the range from fromHash (exclusive) to toHash (inclusive).
577593
/// </summary>

0 commit comments

Comments
 (0)