Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
56 changes: 56 additions & 0 deletions src/release-notes/GitHubReleaseClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Net.Http.Headers;
using System.Text.Json.Serialization;

namespace ReleaseNotes;

/// <summary>
/// Minimal, source-generated-JSON client for creating a GitHub release directly against the REST API.
/// </summary>
/// <remarks>
/// Octokit.Repository.Release.Create can't be used here: Octokit's SimpleJsonSerializer serializes request
/// bodies via raw, unannotated reflection (no [DynamicallyAccessedMembers] anywhere in Octokit/SimpleJson.cs),
/// and under Native AOT trimming the linker strips getter metadata for any property whose accessors have
/// mixed visibility. NewRelease.TagName is "public get; private set;" - the only such property among every
/// request model this tool sends - so in an AOT-published build tag_name silently vanishes from the outgoing
/// JSON while every other field (all plain public get/set) still serializes fine. GitHub then rejects the
/// request with 422 "tag_name" wasn't supplied, even though the C# call site clearly set it. Everything else
/// this tool does through Octokit (GetLatest/Get, ReleaseUpdate, labels) uses only plain public get/set
/// models, so those aren't affected - only the Create path needs this workaround.
/// </remarks>
internal static class GitHubReleaseClient
{
public static async Task CreateRelease(HttpClient httpClient, string owner, string repository, string tagName, string? body, string? token)
{
using var request = new HttpRequestMessage(HttpMethod.Post, $"https://api.github.com/repos/{owner}/{repository}/releases")
{
Content = System.Net.Http.Json.JsonContent.Create(
new GitHubNewReleaseRequest { TagName = tagName, Body = body },
GitHubReleaseJsonContext.Default.GitHubNewReleaseRequest)
};
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("ReleaseNotesGenerator", "1.0"));
request.Headers.Add("X-GitHub-Api-Version", "2022-11-28");
if (token is { Length: > 0 })
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);

using var response = await httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
var responseBody = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException(
$"Failed to create GitHub release for tag '{tagName}' on {owner}/{repository}: {(int)response.StatusCode} {response.StatusCode}\n{responseBody}");
}
}
}

internal sealed class GitHubNewReleaseRequest
{
[JsonPropertyName("tag_name")]
public required string TagName { get; set; }

[JsonPropertyName("body")]
public string? Body { get; set; }
}

[JsonSerializable(typeof(GitHubNewReleaseRequest))]
internal partial class GitHubReleaseJsonContext : JsonSerializerContext;
7 changes: 6 additions & 1 deletion src/release-notes/ReleaseNotesRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ private static async Task CreateRelease(ReleaseNotesConfig config, GitHubClient
await client.Repository.Release.Edit(config.GitHub.Owner, config.GitHub.Repository, existing.Id, new ReleaseUpdate { Body = body.ToString() });
}
else
await client.Repository.Release.Create(config.GitHub.Owner, config.GitHub.Repository, new NewRelease(config.Version) { Body = body.ToString() });
{
// Not client.Repository.Release.Create() - see GitHubReleaseClient's remarks for why Octokit's
// own NewRelease serialization silently drops tag_name under Native AOT.
using var httpClient = new HttpClient();
await GitHubReleaseClient.CreateRelease(httpClient, config.GitHub.Owner, config.GitHub.Repository, config.Version, body.ToString(), config.Token);
}
}

private static async Task<string?> LocateOldVersion(ReleaseNotesConfig config, GitHubClient client)
Expand Down
Loading