Skip to content

Commit be7f7b1

Browse files
jeffklCopilot
andcommitted
Use tags as fallback when release branches are deleted
When generating changelogs, the tool resolves release versions to commit SHAs by looking up release branches (e.g., release/7.5.x). Old release branches get deleted, causing failures. This change adds tag-based fallback: if a branch is not found, the tool finds the latest tag matching the major.minor version pattern (e.g., 7.5.x.x) and uses its commit SHA instead. - Add ResolveVersionToCommitSha and ResolveBranchToCommitSha helpers - Refactor GetLatestTagForMajorMinor to use Version.TryParse for robustness - Add same-commit dedup: if all tags share a commit, use the earliest tag - Update GetCommitsForRelease to resolve SHAs before comparing - Update ChangeLogGenerator to use branch-to-tag fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 292a44a commit be7f7b1

2 files changed

Lines changed: 91 additions & 16 deletions

File tree

NuGetReleaseTool/NuGetReleaseTool/GenerateInsertionChangelogCommand/ChangeLogGenerator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ public static async Task GenerateInsertionChangelogForNuGetClient(GitHubClient g
1212
var repoName = "nuget.client";
1313
string[] issueRepositories = new string[] { "NuGet/Home", "NuGet/Client.Engineering" };
1414

15-
var githubBranch = await gitHubClient.Repository.Branch.Get(orgName, repoName, branchName);
16-
var githubCommits = (await gitHubClient.Repository.Commit.Compare(orgName, repoName, startSha, githubBranch.Commit.Sha)).Commits.Reverse();
15+
var branchSha = await Helpers.ResolveBranchToCommitSha(gitHubClient, orgName, repoName, branchName);
16+
var githubCommits = (await gitHubClient.Repository.Commit.Compare(orgName, repoName, startSha, branchSha)).Commits.Reverse();
1717
List<CommitWithDetails> commits = await Helpers.GetCommitDetails(gitHubClient, orgName, repoName, issueRepositories, githubCommits);
1818
Helpers.SaveAsHtml(commits, resultPath);
1919
Helpers.SaveAsMarkdown(commits, resultPath);

NuGetReleaseTool/NuGetReleaseTool/Helpers.cs

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,35 @@ private static string GetReleaseBranchFromVersion(Version parsedVersion)
2424
return $"release-{parsedVersion.Major}.{parsedVersion.Minor}.x";
2525
}
2626

27-
public static string GetLatestTagForMajorMinor(Version currentVersion, IReadOnlyList<RepositoryTag> allTags)
27+
public static RepositoryTag GetLatestRepositoryTagForMajorMinor(Version currentVersion, IReadOnlyList<RepositoryTag> allTags)
2828
{
29-
var latestTag = allTags.Where(e => e.Name.StartsWith($"{currentVersion.Major}.{currentVersion.Minor}")).Select(e => Version.Parse(e.Name)).Max();
30-
if (latestTag != null)
29+
var matchingTags = allTags
30+
.Select(e => new { Tag = e, Parsed = Version.TryParse(e.Name, out var v) ? v : null })
31+
.Where(e => e.Parsed != null && e.Parsed.Major == currentVersion.Major && e.Parsed.Minor == currentVersion.Minor)
32+
.OrderByDescending(e => e.Parsed)
33+
.ToList();
34+
35+
if (matchingTags.Count == 0)
36+
{
37+
throw new InvalidOperationException($"The {currentVersion} does not have any tags");
38+
}
39+
40+
// If the latest tag shares its commit with older tags (scheduled builds that didn't
41+
// pick up new changes), walk back to the earliest tag on that same commit, which
42+
// represents the original release. Otherwise use the latest tag.
43+
var latestCommitSha = matchingTags.First().Tag.Commit.Sha;
44+
var earliestWithSameCommit = matchingTags.Last(t => t.Tag.Commit.Sha == latestCommitSha);
45+
if (earliestWithSameCommit != matchingTags.First())
3146
{
32-
return latestTag.ToString();
47+
Console.WriteLine($"Tags '{earliestWithSameCommit.Tag.Name}' through '{matchingTags.First().Tag.Name}' point to the same commit. Using earliest tag '{earliestWithSameCommit.Tag.Name}'.");
3348
}
34-
throw new InvalidOperationException($"The {currentVersion} does not have any tags");
49+
50+
return earliestWithSameCommit.Tag;
51+
}
52+
53+
public static string GetLatestTagForMajorMinor(Version currentVersion, IReadOnlyList<RepositoryTag> allTags)
54+
{
55+
return GetLatestRepositoryTagForMajorMinor(currentVersion, allTags).Name;
3556
}
3657

3758
public static Version EstimatePreviousMajorMinorVersion(Version currentVersion, IReadOnlyList<RepositoryTag> allTags)
@@ -64,27 +85,81 @@ public static Version EstimatePreviousMajorMinorVersion(Version currentVersion,
6485
return largestApplicableVersion;
6586
}
6687

88+
/// <summary>
89+
/// Resolves a release version to a commit SHA by trying the release branch first,
90+
/// then falling back to the latest tag matching the major.minor version.
91+
/// This handles cases where release branches have been deleted but tags remain.
92+
/// </summary>
93+
public static async Task<string> ResolveVersionToCommitSha(GitHubClient gitHubClient, string orgName, string repoName, Version version, IReadOnlyList<RepositoryTag> allTags)
94+
{
95+
var branchName = GetReleaseBranchFromVersion(version);
96+
try
97+
{
98+
var branch = await gitHubClient.Repository.Branch.Get(orgName, repoName, branchName);
99+
return branch.Commit.Sha;
100+
}
101+
catch (Octokit.NotFoundException)
102+
{
103+
// Branch doesn't exist (may have been deleted), fall back to latest tag
104+
}
105+
106+
var latestTag = GetLatestRepositoryTagForMajorMinor(version, allTags);
107+
Console.WriteLine($"Branch '{branchName}' not found. Using tag '{latestTag.Name}' instead.");
108+
return latestTag.Commit.Sha;
109+
}
110+
111+
/// <summary>
112+
/// Resolves a branch name to a commit SHA by trying the branch first,
113+
/// then falling back to the latest matching tag if the branch name matches a release pattern.
114+
/// </summary>
115+
public static async Task<string> ResolveBranchToCommitSha(GitHubClient gitHubClient, string orgName, string repoName, string branchName)
116+
{
117+
try
118+
{
119+
var branch = await gitHubClient.Repository.Branch.Get(orgName, repoName, branchName);
120+
return branch.Commit.Sha;
121+
}
122+
catch (Octokit.NotFoundException)
123+
{
124+
// Branch doesn't exist (may have been deleted), try tag fallback
125+
}
126+
127+
var match = Regex.Match(branchName, @"release[/-](\d+)\.(\d+)\.x$");
128+
if (match.Success)
129+
{
130+
var version = new Version(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value));
131+
var allTags = await gitHubClient.Repository.GetAllTags(orgName, repoName);
132+
var latestTag = GetLatestRepositoryTagForMajorMinor(version, allTags);
133+
Console.WriteLine($"Branch '{branchName}' not found. Using tag '{latestTag.Name}' instead.");
134+
return latestTag.Commit.Sha;
135+
}
136+
137+
throw new InvalidOperationException($"Branch '{branchName}' was not found and does not match a recognized release branch pattern for tag fallback.");
138+
}
139+
67140
public static async Task<List<GitHubCommit>> GetCommitsForRelease(GitHubClient gitHubClient, string releaseVersion, string? endCommit)
68141
{
69142
var version = new Version(releaseVersion);
70-
var currentReleaseBranchName = GetReleaseBranchFromVersion(version);
71143
IReadOnlyList<Milestone> milestones = await gitHubClient.Issue.Milestone.GetAllForRepository(Constants.NuGet, Constants.Home, new MilestoneRequest { State = ItemStateFilter.All });
72-
var previousReleaseBranchName = GetReleaseBranchFromVersion(EstimatePreviousMajorMinorVersion(version, milestones));
73-
return await GetUniqueCommitsListBetween2Branches(gitHubClient, Constants.NuGet, Constants.NuGetClient, previousReleaseBranchName, currentReleaseBranchName, endCommit);
144+
var previousVersion = EstimatePreviousMajorMinorVersion(version, milestones);
145+
146+
var allTags = await gitHubClient.Repository.GetAllTags(Constants.NuGet, Constants.NuGetClient);
147+
148+
var previousSha = await ResolveVersionToCommitSha(gitHubClient, Constants.NuGet, Constants.NuGetClient, previousVersion, allTags);
149+
var currentSha = endCommit ?? await ResolveVersionToCommitSha(gitHubClient, Constants.NuGet, Constants.NuGetClient, version, allTags);
150+
151+
return await GetUniqueCommitsListBetween2Refs(gitHubClient, Constants.NuGet, Constants.NuGetClient, previousSha, currentSha);
74152
}
75153

76-
public static async Task<List<GitHubCommit>> GetUniqueCommitsListBetween2Branches(GitHubClient gitHubClient, string orgName, string repoName, string previousBranchName, string currentBranchName, string? latestShaOnCurrentBranch = null)
154+
public static async Task<List<GitHubCommit>> GetUniqueCommitsListBetween2Refs(GitHubClient gitHubClient, string orgName, string repoName, string baseSha, string headSha)
77155
{
78-
var previousBranch = await gitHubClient.Repository.Branch.Get(orgName, repoName, previousBranchName);
79-
var currentBranch = await gitHubClient.Repository.Branch.Get(orgName, repoName, currentBranchName);
80156
// Reverse so that the oldest commit is at the top.
81-
string latestShaToUse = latestShaOnCurrentBranch ?? currentBranch.Commit.Sha;
82-
var allCommitDifference = (await gitHubClient.Repository.Commit.Compare(orgName, repoName, previousBranch.Commit.Sha, latestShaToUse)).Commits.Reverse();
157+
var allCommitDifference = (await gitHubClient.Repository.Commit.Compare(orgName, repoName, baseSha, headSha)).Commits.Reverse();
83158

84159
var commitsOnReleaseBranchSince = await gitHubClient.Repository.Commit.GetAll(orgName, repoName, new CommitRequest
85160
{
86161
Since = allCommitDifference.Min(e => e.Commit.Committer.Date), // Find the oldest commit in the delta
87-
Sha = previousBranch.Commit.Sha
162+
Sha = baseSha
88163
});
89164

90165
List<GitHubCommit> gitHubCommits = new();

0 commit comments

Comments
 (0)