Skip to content

Commit 7be635d

Browse files
rojiCopilot
andcommitted
Infer version from tags/branches instead of Versions.props
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: - release/X.Y (servicing): list vX.Y.* tags via GitHub API, find the highest GA patch (< 100), set milestone to X.Y.(patch+1). No preview/rc label. - main: read only major.minor from Versions.props, then list release/X.Y-* branches to find the highest preview/rc branch. The next version is inferred: preview7→rc-1, rc1→rc-2, rc2→GA (no label). Milestone is X.Y.0. 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 a81cc59 commit 7be635d

1 file changed

Lines changed: 86 additions & 28 deletions

File tree

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

Lines changed: 86 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 rather than 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,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

Comments
 (0)