11# This workflow automatically labels issues with the preview/RC version when their fixing PR
22# is merged into main or a release branch, and sets their milestone to the current version.
3- # All version information is read from eng/Versions.props at the merge commit .
3+ # The version is inferred from tags and branches in the repo rather than eng/Versions.props .
44
55name : Label and milestone closed issues
66
2828 const owner = context.repo.owner;
2929 const repo = context.repo.repo;
3030 const prNumber = context.payload.pull_request.number;
31- const mergeCommitSha = context.payload.pull_request.merge_commit_sha;
3231
3332 // Find issues closed by this PR using GraphQL (include current milestone)
3433 const query = `
@@ -56,30 +55,90 @@ jobs:
5655 return;
5756 }
5857
59- // Read version info from eng/Versions.props at the actual merge commit
60- const { data: versionFileData } = await github.rest.repos.getContent({
61- owner,
62- repo,
63- path: 'eng/Versions.props',
64- ref: mergeCommitSha
65- });
66- const versionFileContent = Buffer.from(versionFileData.content, 'base64').toString('utf-8');
67-
68- const versionPrefixMatch = versionFileContent.match(/<VersionPrefix>(\d+\.\d+\.\d+)<\/VersionPrefix>/);
69- if (!versionPrefixMatch) {
70- throw new Error('Could not parse VersionPrefix from eng/Versions.props');
71- }
72- const versionPrefix = versionPrefixMatch[1];
58+ // Detect version and label from tags/branches based on the target branch
59+ const targetBranch = context.payload.pull_request.base.ref;
60+ let targetMilestoneName;
61+ let label;
7362
74- const preReleaseLabelMatch = versionFileContent.match(/<PreReleaseVersionLabel>(preview|rc)<\/PreReleaseVersionLabel>/);
75- const preReleaseIterationMatch = versionFileContent.match(/<PreReleaseVersionIteration>(\d+)<\/PreReleaseVersionIteration>/);
63+ const releaseBranchMatch = targetBranch.match(/^release\/(\d+)\.(\d+)$/);
64+ if (releaseBranchMatch) {
65+ // Servicing branch (e.g. release/10.0): find the next patch version from tags
66+ const major = releaseBranchMatch[1];
67+ const minor = releaseBranchMatch[2];
68+
69+ const { data: tagRefs } = await github.rest.git.listMatchingRefs({
70+ owner,
71+ repo,
72+ ref: `tags/v${major}.${minor}.`
73+ });
74+
75+ let highestPatch = -1;
76+ for (const ref of tagRefs) {
77+ const m = ref.ref.match(/^refs\/tags\/v\d+\.\d+\.(\d+)$/);
78+ if (m) {
79+ const patch = parseInt(m[1]);
80+ if (patch < 100 && patch > highestPatch) {
81+ highestPatch = patch;
82+ }
83+ }
84+ }
7685
77- let label;
78- if (preReleaseLabelMatch && preReleaseIterationMatch) {
79- label = `${preReleaseLabelMatch[1]}-${preReleaseIterationMatch[1]}`;
86+ targetMilestoneName = `${major}.${minor}.${highestPatch + 1}`;
87+ // No preview/rc label for servicing branches
88+ } else if (targetBranch === 'main') {
89+ // Main branch: read major.minor from Versions.props, then infer
90+ // the next preview/rc from existing release branches
91+ const { data: versionFileData } = await github.rest.repos.getContent({
92+ owner,
93+ repo,
94+ path: 'eng/Versions.props',
95+ ref: context.payload.pull_request.merge_commit_sha
96+ });
97+ const versionFileContent = Buffer.from(versionFileData.content, 'base64').toString('utf-8');
98+ const versionPrefixMatch = versionFileContent.match(/<VersionPrefix>(\d+)\.(\d+)\.\d+<\/VersionPrefix>/);
99+ if (!versionPrefixMatch) {
100+ throw new Error('Could not parse VersionPrefix from eng/Versions.props');
101+ }
102+ const major = versionPrefixMatch[1];
103+ const minor = versionPrefixMatch[2];
104+
105+ targetMilestoneName = `${major}.${minor}.0`;
106+
107+ // List release branches for this major.minor to find what's already been branched
108+ const { data: branchRefs } = await github.rest.git.listMatchingRefs({
109+ owner,
110+ repo,
111+ ref: `heads/release/${major}.${minor}-`
112+ });
113+
114+ let highestPreview = 0;
115+ let highestRc = 0;
116+ for (const ref of branchRefs) {
117+ const previewMatch = ref.ref.match(/^refs\/heads\/release\/\d+\.\d+-preview(\d+)$/);
118+ if (previewMatch) {
119+ highestPreview = Math.max(highestPreview, parseInt(previewMatch[1]));
120+ continue;
121+ }
122+ const rcMatch = ref.ref.match(/^refs\/heads\/release\/\d+\.\d+-rc(\d+)$/);
123+ if (rcMatch) {
124+ highestRc = Math.max(highestRc, parseInt(rcMatch[1]));
125+ }
126+ }
127+
128+ if (highestRc >= 2) {
129+ // After rc2, we're heading to GA — no label
130+ } else if (highestRc === 1) {
131+ label = 'rc-2';
132+ } else if (highestPreview >= 7) {
133+ label = 'rc-1';
134+ } else {
135+ label = `preview-${highestPreview + 1}`;
136+ }
137+ } else {
138+ throw new Error(`Unexpected target branch: ${targetBranch}`);
80139 }
81140
82- console.log(`Version : ${versionPrefix }, label: ${label ?? 'none'}`);
141+ console.log(`Target branch : ${targetBranch}, milestone: ${targetMilestoneName }, label: ${label ?? 'none'}`);
83142
84143 // Label all closing issues
85144 // (don't filter by state to avoid race conditions where GitHub
@@ -101,11 +160,10 @@ jobs:
101160 }
102161 }
103162
104- // Look up the target milestone (e.g. "11.0.0", "10.0.5") via GraphQL,
105- // including closed milestones to avoid recreating one that already exists.
106- // The GraphQL query parameter does fuzzy/substring matching (no exact match
107- // option), so we fetch multiple results and filter client-side.
108- const targetMilestoneName = versionPrefix;
163+ // Look up the target milestone via GraphQL, including closed milestones
164+ // to avoid recreating one that already exists. The GraphQL query parameter
165+ // does fuzzy/substring matching (no exact match option), so we fetch
166+ // multiple results and filter client-side.
109167 const milestoneResult = await github.graphql(`
110168 query($owner: String!, $repo: String!, $title: String!) {
111169 repository(owner: $owner, name: $repo) {
@@ -136,7 +194,7 @@ jobs:
136194
137195 // Set the milestone on closing issues, applying a "min" strategy:
138196 // only update if the issue has no version milestone or the target is earlier
139- const targetVersion = parseVersion(versionPrefix );
197+ const targetVersion = parseVersion(targetMilestoneName );
140198 for (const issue of closingIssues) {
141199 const currentTitle = issue.milestone?.title;
142200 const currentVersion = currentTitle ? parseVersion(currentTitle) : null;
0 commit comments