@@ -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