Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Octokit" Version="14.0.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.19.0.132793">
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,78 @@ ... on Commit {
}
}

/// <summary>
/// Gets all releases for a repository using GraphQL with pagination.
/// </summary>
/// <param name="owner">Repository owner.</param>
/// <param name="repo">Repository name.</param>
/// <returns>List of release tag names.</returns>
public async Task<List<string>> GetReleasesAsync(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not have this return a list of ReleaseNode records?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e2a22ca. Changed GetReleasesAsync to return List<ReleaseNode> directly, eliminating the intermediate string conversion.

string owner,
string repo)
{
try
{
var allReleaseTagNames = new List<string>();
string? afterCursor = null;
bool hasNextPage;

// Paginate through all releases
do
{
// Create GraphQL request to get releases for a repository with pagination support
var request = new GraphQLRequest
{
Query = @"
query($owner: String!, $repo: String!, $after: String) {
repository(owner: $owner, name: $repo) {
releases(first: 100, after: $after, orderBy: {field: CREATED_AT, direction: DESC}) {
nodes {
tagName
}
pageInfo {
hasNextPage
endCursor
}
}
}
}",
Variables = new
{
owner,
repo,
after = afterCursor
}
};

// Execute GraphQL query
var response = await _graphqlClient.SendQueryAsync<GetReleasesResponse>(request);

// Extract release tag names from the GraphQL response, filtering out null or invalid values
var pageReleaseTagNames = response.Data?.Repository?.Releases?.Nodes?
.Where(n => !string.IsNullOrEmpty(n.TagName))
.Select(n => n.TagName!)
.ToList() ?? [];

allReleaseTagNames.AddRange(pageReleaseTagNames);

// Check if there are more pages
var pageInfo = response.Data?.Repository?.Releases?.PageInfo;
hasNextPage = pageInfo?.HasNextPage ?? false;
afterCursor = pageInfo?.EndCursor;
}
while (hasNextPage);

// Return list of all release tag names
return allReleaseTagNames;
}
catch
{
// If GraphQL query fails, return empty list
return [];
}
}

/// <summary>
/// Finds issue IDs linked to a pull request via closingIssuesReferences.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,33 @@ internal record CommitHistoryData(
/// <param name="Oid">Git object ID (SHA).</param>
internal record CommitNode(
string? Oid);

/// <summary>
/// Response for getting releases from a repository.
/// </summary>
/// <param name="Repository">Repository data containing release information.</param>
internal record GetReleasesResponse(
ReleaseRepositoryData? Repository);

/// <summary>
/// Repository data containing releases information.
/// </summary>
/// <param name="Releases">Releases connection data.</param>
internal record ReleaseRepositoryData(
ReleasesConnectionData? Releases);

/// <summary>
/// Releases connection data containing nodes and page info.
/// </summary>
/// <param name="Nodes">Release nodes.</param>
/// <param name="PageInfo">Pagination information.</param>
internal record ReleasesConnectionData(
List<ReleaseNode>? Nodes,
PageInfo? PageInfo);

/// <summary>
/// Release node containing release information.
/// </summary>
/// <param name="TagName">Tag name associated with the release.</param>
internal record ReleaseNode(
string? TagName);
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@
{
// Fetch all data from GitHub in parallel
var commitsTask = GetAllCommitsAsync(graphqlClient, owner, repo, branch);
var releasesTask = client.Repository.Release.GetAll(owner, repo);
var releasesTask = GetAllReleasesAsync(graphqlClient, owner, repo);
var tagsTask = client.Repository.GetAllTags(owner, repo);
var pullRequestsTask = client.PullRequest.GetAllForRepository(owner, repo, new PullRequestRequest { State = ItemStateFilter.All });
var issuesTask = client.Issue.GetAllForRepository(owner, repo, new RepositoryIssueRequest { State = ItemStateFilter.All });
Expand Down Expand Up @@ -572,6 +572,33 @@
return commitShas.Select(sha => new Commit(sha)).ToList();
}

/// <summary>
/// Gets all releases for a repository using GraphQL pagination.
/// </summary>
/// <param name="graphqlClient">GitHub GraphQL client.</param>
/// <param name="owner">Repository owner.</param>
/// <param name="repo">Repository name.</param>
/// <returns>List of all releases.</returns>
private static async Task<IReadOnlyList<Release>> GetAllReleasesAsync(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems there are two data types introduced - ReleaseNode and Release, and the code goes from ReleaseNode to string, to release. Don't add extra types and extra conversions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e2a22ca. Removed the custom Release type and the extra conversions. Now GetReleasesAsync returns List<ReleaseNode> directly, and ReleaseNode is used throughout the application (in GitHubData, LookupData, etc.). The flow is now: GraphQL response → ReleaseNode → used directly.

GitHubGraphQLClient graphqlClient,
string owner,
string repo)
{
// Fetch all release tag names for the repository using GraphQL
var releaseTagNames = await graphqlClient.GetReleasesAsync(owner, repo);

// Convert tag names to Release objects using JSON deserialization
// This creates minimal Release objects with only TagName populated
return releaseTagNames.Select(tagName =>
Comment thread
Malcolmnixon marked this conversation as resolved.
Outdated
{
// Use JsonConvert with a proper object to avoid JSON injection
var releaseData = new { tag_name = tagName };
var json = Newtonsoft.Json.JsonConvert.SerializeObject(releaseData);

Check warning on line 596 in src/DemaConsulting.BuildMark/RepoConnectors/GitHubRepoConnector.cs

View workflow job for this annotation

GitHub Actions / Build / Quality Checks

Unknown word (Newtonsoft)
var release = Newtonsoft.Json.JsonConvert.DeserializeObject<Release>(json);

Check warning on line 597 in src/DemaConsulting.BuildMark/RepoConnectors/GitHubRepoConnector.cs

View workflow job for this annotation

GitHub Actions / Build / Quality Checks

Unknown word (Newtonsoft)
return release ?? throw new InvalidOperationException($"Failed to create Release object for tag {tagName}");
}).ToList();
}

/// <summary>
/// Gets commits in the range from fromHash (exclusive) to toHash (inclusive).
/// </summary>
Expand Down
Loading
Loading