Skip to content

Commit f8d2760

Browse files
rojiCopilot
andauthored
Infer version from tags/branches instead of Versions.props (#37985)
Replace the eng/Versions.props-based version detection in the label-and-milestone-issues workflow with logic that infers versions from tags and branches present in the repo. This fixes the issue where Versions.props was frequently updated too late and didn't represent the correct version at merge time. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 97db209 commit f8d2760

1 file changed

Lines changed: 94 additions & 28 deletions

File tree

.github/workflows/label-and-milestone-issues.yml

Lines changed: 94 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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, with the major/minor on main taken from eng/Versions.props.
44

55
name: Label and milestone closed issues
66

@@ -28,7 +28,6 @@ jobs:
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,98 @@ 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];
7668
77-
let label;
78-
if (preReleaseLabelMatch && preReleaseIterationMatch) {
79-
label = `${preReleaseLabelMatch[1]}-${preReleaseIterationMatch[1]}`;
69+
const tagRefs = await github.paginate(
70+
github.rest.git.listMatchingRefs,
71+
{
72+
owner,
73+
repo,
74+
ref: `tags/v${major}.${minor}.`,
75+
per_page: 100
76+
}
77+
);
78+
79+
let highestPatch = -1;
80+
for (const ref of tagRefs) {
81+
const m = ref.ref.match(/^refs\/tags\/v\d+\.\d+\.(\d+)$/);
82+
if (m) {
83+
const patch = parseInt(m[1]);
84+
if (patch < 100 && patch > highestPatch) {
85+
highestPatch = patch;
86+
}
87+
}
88+
}
89+
90+
targetMilestoneName = `${major}.${minor}.${highestPatch + 1}`;
91+
// No preview/rc label for servicing branches
92+
} else if (targetBranch === 'main') {
93+
// Main branch: read major.minor from Versions.props, then infer
94+
// the next preview/rc from existing release branches
95+
const { data: versionFileData } = await github.rest.repos.getContent({
96+
owner,
97+
repo,
98+
path: 'eng/Versions.props',
99+
ref: context.payload.pull_request.merge_commit_sha
100+
});
101+
const versionFileContent = Buffer.from(versionFileData.content, 'base64').toString('utf-8');
102+
const versionPrefixMatch = versionFileContent.match(/<VersionPrefix>(\d+)\.(\d+)\.\d+<\/VersionPrefix>/);
103+
if (!versionPrefixMatch) {
104+
throw new Error('Could not parse VersionPrefix from eng/Versions.props');
105+
}
106+
const major = versionPrefixMatch[1];
107+
const minor = versionPrefixMatch[2];
108+
109+
targetMilestoneName = `${major}.${minor}.0`;
110+
111+
// List release branches for this major.minor to find what's already been branched
112+
const branchRefs = await github.paginate(
113+
github.rest.git.listMatchingRefs,
114+
{
115+
owner,
116+
repo,
117+
ref: `heads/release/${major}.${minor}-`,
118+
per_page: 100
119+
}
120+
);
121+
122+
let highestPreview = 0;
123+
let highestRc = 0;
124+
for (const ref of branchRefs) {
125+
const previewMatch = ref.ref.match(/^refs\/heads\/release\/\d+\.\d+-preview(\d+)$/);
126+
if (previewMatch) {
127+
highestPreview = Math.max(highestPreview, parseInt(previewMatch[1]));
128+
continue;
129+
}
130+
const rcMatch = ref.ref.match(/^refs\/heads\/release\/\d+\.\d+-rc(\d+)$/);
131+
if (rcMatch) {
132+
highestRc = Math.max(highestRc, parseInt(rcMatch[1]));
133+
}
134+
}
135+
136+
if (highestRc >= 2) {
137+
// After rc2, we're heading to GA — no label
138+
} else if (highestRc === 1) {
139+
label = 'rc-2';
140+
} else if (highestPreview >= 7) {
141+
label = 'rc-1';
142+
} else {
143+
label = `preview-${highestPreview + 1}`;
144+
}
145+
} else {
146+
throw new Error(`Unexpected target branch: ${targetBranch}`);
80147
}
81148
82-
console.log(`Version: ${versionPrefix}, label: ${label ?? 'none'}`);
149+
console.log(`Target branch: ${targetBranch}, milestone: ${targetMilestoneName}, label: ${label ?? 'none'}`);
83150
84151
// Label all closing issues
85152
// (don't filter by state to avoid race conditions where GitHub
@@ -101,11 +168,10 @@ jobs:
101168
}
102169
}
103170
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;
171+
// Look up the target milestone via GraphQL, including closed milestones
172+
// to avoid recreating one that already exists. The GraphQL query parameter
173+
// does fuzzy/substring matching (no exact match option), so we fetch
174+
// multiple results and filter client-side.
109175
const milestoneResult = await github.graphql(`
110176
query($owner: String!, $repo: String!, $title: String!) {
111177
repository(owner: $owner, name: $repo) {
@@ -136,7 +202,7 @@ jobs:
136202
137203
// Set the milestone on closing issues, applying a "min" strategy:
138204
// only update if the issue has no version milestone or the target is earlier
139-
const targetVersion = parseVersion(versionPrefix);
205+
const targetVersion = parseVersion(targetMilestoneName);
140206
for (const issue of closingIssues) {
141207
const currentTitle = issue.milestone?.title;
142208
const currentVersion = currentTitle ? parseVersion(currentTitle) : null;

0 commit comments

Comments
 (0)