Skip to content

Commit b319a0b

Browse files
authored
Fix duplication of commit descriptions and missing PRs in release notes. (#5182)
* Fix: Prevent duplication of feature descriptions across multiple Agent releases. * removed additional new lines and renamed function. * removed extra space from GraphQL query. * adding package-lock.json * changes made to consider fetchingPR based on derivedFrom field. * making default value to empty to consider fetching PR from previous release. * Added a descriptive comment and set the default value to lastMinorRelease. * removed extra spaces
1 parent 39b50b9 commit b319a0b

File tree

4 files changed

+1770
-82
lines changed

4 files changed

+1770
-82
lines changed

.vsts.release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ parameters:
3434
- name: derivedFrom
3535
type: string
3636
displayName: Derived From Version
37-
default: latest
37+
default: 'lastMinorRelease'
3838
- name: skipTests
3939
type: boolean
4040
default: false

release/createReleaseBranch.js

Lines changed: 162 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,46 @@ const tl = require('azure-pipelines-task-lib/task');
55
const util = require('./util');
66

77
const { Octokit } = require("@octokit/rest");
8+
const { graphql } = require("@octokit/graphql");
9+
const fetch = require('node-fetch');
810

911
const OWNER = 'microsoft';
10-
const REPO = 'azure-pipelines-agent';
12+
const REPO = 'azure-pipelines-agent';
1113
const GIT = 'git';
1214
const VALID_RELEASE_RE = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/;
1315
const octokit = new Octokit({}); // only read-only operations, no need to auth
1416

17+
const graphqlWithFetch = graphql.defaults({ // Create a reusable GraphQL instance with fetch
18+
request: {
19+
fetch,
20+
},
21+
headers: {
22+
authorization: process.env.PAT ? `token ${process.env.PAT}` : undefined,
23+
}
24+
});
25+
1526
process.env.EDITOR = process.env.EDITOR === undefined ? 'code --wait' : process.env.EDITOR;
1627

1728
var opt = require('node-getopt').create([
18-
['', 'dryrun', 'Dry run only, do not actually commit new release'],
19-
['', 'derivedFrom=version', 'Used to get PRs merged since this release was created', 'latest'],
20-
['', 'branch=branch', 'Branch to select PRs merged into', 'master'],
21-
['h', 'help', 'Display this help'],
22-
])
23-
.setHelp(
24-
'Usage: node createReleaseBranch.js [OPTION] <version>\n' +
25-
'\n' +
26-
'[[OPTIONS]]\n'
27-
)
28-
.bindHelp() // bind option 'help' to default action
29-
.parseSystem(); // parse command line
30-
31-
async function verifyNewReleaseTagOk(newRelease)
32-
{
33-
if (!newRelease || !newRelease.match(VALID_RELEASE_RE) || newRelease.endsWith('.999.999'))
34-
{
29+
['', 'dryrun', 'Dry run only, do not actually commit new release'],
30+
['', 'derivedFrom=version', 'Used to get PRs merged since this release was created', 'lastMinorRelease'],
31+
['', 'branch=branch', 'Branch to select PRs merged into', 'master'],
32+
['h', 'help', 'Display this help'],
33+
])
34+
.setHelp(
35+
'Usage: node createReleaseBranch.js [OPTION] <version>\n' +
36+
'\n' +
37+
'[[OPTIONS]]\n'
38+
)
39+
.bindHelp() // bind option 'help' to default action
40+
.parseSystem(); // parse command line
41+
42+
async function verifyNewReleaseTagOk(newRelease) {
43+
if (!newRelease || !newRelease.match(VALID_RELEASE_RE) || newRelease.endsWith('.999.999')) {
3544
console.log(`Invalid version '${newRelease}'. Version must be in the form of <major>.<minor>.<patch> where each level is 0-999`);
3645
process.exit(-1);
3746
}
38-
try
39-
{
47+
try {
4048
var tag = 'v' + newRelease;
4149
await octokit.repos.getReleaseByTag({
4250
owner: OWNER,
@@ -47,33 +55,129 @@ async function verifyNewReleaseTagOk(newRelease)
4755
console.log(`Version ${newRelease} is already in use`);
4856
process.exit(-1);
4957
}
50-
catch
51-
{
58+
catch {
5259
console.log(`Version ${newRelease} is available for use`);
5360
}
5461
}
5562

56-
function writeAgentVersionFile(newRelease)
57-
{
63+
function writeAgentVersionFile(newRelease) {
5864
console.log('Writing agent version file')
59-
if (!opt.options.dryrun)
60-
{
65+
if (!opt.options.dryrun) {
6166
fs.writeFileSync(path.join(__dirname, '..', 'src', 'agentversion'), `${newRelease}\n`);
6267
}
6368
return newRelease;
6469
}
6570

66-
async function fetchPRsSinceLastReleaseAndEditReleaseNotes(newRelease, callback)
67-
{
71+
async function fetchPRsForSHAsGraphQL(commitSHAs) {
72+
73+
var queryParts = commitSHAs.map((sha, index) => `
74+
commit${index + 1}: object(expression: "${sha}") { ... on Commit { associatedPullRequests(first: 1) {
75+
edges { node { title number createdAt closedAt labels(first: 10) { edges { node { name } } } } } } } }`);
76+
77+
var fullQuery = `
78+
query ($repo: String!, $owner: String!) {
79+
repository(name: $repo, owner: $owner) {
80+
${queryParts.join('\n')}
81+
}
82+
}
83+
`;
84+
85+
try {
86+
var response = await graphqlWithFetch(fullQuery, {
87+
repo: REPO,
88+
owner: OWNER,
89+
});
90+
91+
var prs = [];
92+
Object.keys(response.repository).forEach(commitKey => {
93+
var commit = response.repository[commitKey];
94+
if (commit && commit.associatedPullRequests) {
95+
commit.associatedPullRequests.edges.forEach(pr => {
96+
prs.push({
97+
title: pr.node.title,
98+
number: pr.node.number,
99+
createdAt: pr.node.createdAt,
100+
closedAt: pr.node.closedAt,
101+
labels: pr.node.labels.edges.map(label => ({ name: label.node.name })), // Extract label names
102+
});
103+
});
104+
}
105+
});
106+
return prs;
107+
} catch (e) {
108+
console.log(e);
109+
console.error(`Error fetching PRs via GraphQL.`);
110+
process.exit(-1);
111+
}
112+
}
113+
114+
async function fetchPRsSincePreviousReleaseAndEditReleaseNotes(newRelease, callback) {
115+
try {
116+
var latestReleases = await octokit.repos.listReleases({
117+
owner: OWNER,
118+
repo: REPO
119+
})
120+
121+
var filteredReleases = latestReleases.data.filter(release => !release.draft); // consider only pre-releases and published releases
122+
123+
var releaseTagPrefix = 'v' + newRelease.split('.')[0];
124+
console.log(`Getting latest release starting with ${releaseTagPrefix}`);
125+
126+
var latestReleaseInfo = filteredReleases.find(release => release.tag_name.toLowerCase().startsWith(releaseTagPrefix.toLowerCase()));
127+
console.log(`Previous release tag with ${latestReleaseInfo.tag_name} and published date is: ${latestReleaseInfo.published_at}`)
128+
129+
var headBranchTag = 'v' + newRelease
130+
try {
131+
var comparison = await octokit.repos.compareCommits({
132+
owner: OWNER,
133+
repo: REPO,
134+
base: latestReleaseInfo.tag_name,
135+
head: headBranchTag,
136+
});
137+
138+
var commitSHAs = comparison.data.commits.map(commit => commit.sha);
139+
140+
try {
141+
142+
var allPRs = await fetchPRsForSHAsGraphQL(commitSHAs);
143+
editReleaseNotesFile({ items: allPRs });
144+
} catch (e) {
145+
console.log(e);
146+
console.log(`Error: Problem in fetching PRs using commit SHA. Aborting.`);
147+
process.exit(-1);
148+
}
149+
150+
} catch (e) {
151+
console.log(e);
152+
console.log(`Error: Cannot find commits changes. Aborting.`);
153+
process.exit(-1);
154+
}
155+
}
156+
catch (e) {
157+
console.log(e);
158+
console.log(`Error: Cannot find releases. Aborting.`);
159+
process.exit(-1);
160+
}
161+
}
162+
163+
164+
async function fetchPRsSinceLastReleaseAndEditReleaseNotes(newRelease, callback) {
68165
var derivedFrom = opt.options.derivedFrom;
69166
console.log("Derived from %o", derivedFrom);
70167

71-
try
72-
{
168+
try {
73169
var releaseInfo;
74170

75-
if (derivedFrom !== 'latest')
76-
{
171+
// If derivedFrom is 'lastMinorRelease', fetch PRs by comparing with the previous release.
172+
// For example:
173+
// - If newRelease = 4.255.0, it will compare changes with the latest RELEASE/PRE-RELEASE tag starting with 4.xxx.xxx.
174+
// - If newRelease = 3.255.1, it will compare changes with the latest RELEASE/PRE-RELEASE tag starting with 3.xxx.xxx.
175+
if (derivedFrom === 'lastMinorRelease') {
176+
console.log("Fetching PRs by comparing with the previous release.")
177+
await fetchPRsSincePreviousReleaseAndEditReleaseNotes(newRelease, callback);
178+
return;
179+
}
180+
else if (derivedFrom !== 'latest') {
77181
var tag = 'v' + derivedFrom;
78182

79183
console.log(`Getting release by tag ${tag}`);
@@ -84,8 +188,7 @@ async function fetchPRsSinceLastReleaseAndEditReleaseNotes(newRelease, callback)
84188
tag: tag
85189
});
86190
}
87-
else
88-
{
191+
else {
89192
console.log("Getting latest release");
90193

91194
releaseInfo = await octokit.repos.getLatestRelease({
@@ -97,55 +200,47 @@ async function fetchPRsSinceLastReleaseAndEditReleaseNotes(newRelease, callback)
97200
var branch = opt.options.branch;
98201
var lastReleaseDate = releaseInfo.data.published_at;
99202
console.log(`Fetching PRs merged since ${lastReleaseDate} on ${branch}`);
100-
try
101-
{
203+
try {
102204
var results = await octokit.search.issuesAndPullRequests({
103-
q:`type:pr+is:merged+repo:${OWNER}/${REPO}+base:${branch}+merged:>=${lastReleaseDate}`,
205+
q: `type:pr+is:merged+repo:${OWNER}/${REPO}+base:${branch}+merged:>=${lastReleaseDate}`,
104206
order: 'asc',
105207
sort: 'created'
106208
})
107209
editReleaseNotesFile(results.data);
108210
}
109-
catch (e)
110-
{
211+
catch (e) {
111212
console.log(`Error: Problem fetching PRs: ${e}`);
112213
process.exit(-1);
113214
}
114215
}
115-
catch (e)
116-
{
216+
catch (e) {
117217
console.log(e);
118218
console.log(`Error: Cannot find release ${opt.options.derivedFrom}. Aborting.`);
119219
process.exit(-1);
120220
}
121221
}
122222

123-
function editReleaseNotesFile(body)
124-
{
223+
224+
function editReleaseNotesFile(body) {
125225
var releaseNotesFile = path.join(__dirname, '..', 'releaseNote.md');
126226
var existingReleaseNotes = fs.readFileSync(releaseNotesFile);
127227
var newPRs = { 'Features': [], 'Bugs': [], 'Misc': [] };
128228
body.items.forEach(function (item) {
129229
var category = 'Misc';
130230
item.labels.forEach(function (label) {
131-
if (category)
132-
{
133-
if (label.name === 'bug')
134-
{
231+
if (category) {
232+
if (label.name === 'bug') {
135233
category = 'Bugs';
136234
}
137-
if (label.name === 'enhancement')
138-
{
235+
if (label.name === 'enhancement') {
139236
category = 'Features';
140237
}
141-
if (label.name === 'internal')
142-
{
238+
if (label.name === 'internal') {
143239
category = null;
144240
}
145241
}
146242
});
147-
if (category)
148-
{
243+
if (category) {
149244
newPRs[category].push(` - ${item.title} (#${item.number})`);
150245
}
151246
});
@@ -158,39 +253,33 @@ function editReleaseNotesFile(body)
158253
newReleaseNotes += existingReleaseNotes;
159254
var editorCmd = `${process.env.EDITOR} ${releaseNotesFile}`;
160255
console.log(editorCmd);
161-
if (opt.options.dryrun)
162-
{
256+
if (opt.options.dryrun) {
163257
console.log('Found the following PRs = %o', newPRs);
164258
console.log('\n\n');
165259
console.log(newReleaseNotes);
166260
console.log('\n');
167261
}
168-
else
169-
{
262+
else {
170263
fs.writeFileSync(releaseNotesFile, newReleaseNotes);
171-
try
172-
{
264+
try {
173265
cp.execSync(`${process.env.EDITOR} ${releaseNotesFile}`, {
174266
stdio: [process.stdin, process.stdout, process.stderr]
175267
});
176268
}
177-
catch (err)
178-
{
269+
catch (err) {
179270
console.log(err.message);
180271
process.exit(-1);
181272
}
182273
}
183274
}
184275

185-
function commitAndPush(directory, release, branch)
186-
{
276+
function commitAndPush(directory, release, branch) {
187277
util.execInForeground(GIT + " checkout -b " + branch, directory, opt.options.dryrun);
188278
util.execInForeground(`${GIT} commit -m "Agent Release ${release}" `, directory, opt.options.dryrun);
189279
util.execInForeground(`${GIT} -c credential.helper='!f() { echo "username=pat"; echo "password=$PAT"; };f' push --set-upstream origin ${branch}`, directory, opt.options.dryrun);
190280
}
191281

192-
function commitAgentChanges(directory, release)
193-
{
282+
function commitAgentChanges(directory, release) {
194283
var newBranch = `releases/${release}`;
195284
util.execInForeground(`${GIT} add ${path.join('src', 'agentversion')}`, directory, opt.options.dryrun);
196285
util.execInForeground(`${GIT} add releaseNote.md`, directory, opt.options.dryrun);
@@ -199,31 +288,25 @@ function commitAgentChanges(directory, release)
199288
commitAndPush(directory, release, newBranch);
200289
}
201290

202-
function checkGitStatus()
203-
{
204-
var git_status = cp.execSync(`${GIT} status --untracked-files=no --porcelain`, { encoding: 'utf-8'});
205-
if (git_status)
206-
{
291+
function checkGitStatus() {
292+
var git_status = cp.execSync(`${GIT} status --untracked-files=no --porcelain`, { encoding: 'utf-8' });
293+
if (git_status) {
207294
console.log('You have uncommited changes in this clone. Aborting.');
208295
console.log(git_status);
209-
if (!opt.options.dryrun)
210-
{
296+
if (!opt.options.dryrun) {
211297
process.exit(-1);
212298
}
213299
}
214-
else
215-
{
300+
else {
216301
console.log('Git repo is clean.');
217302
}
218303
return git_status;
219304
}
220305

221-
async function main()
222-
{
306+
async function main() {
223307
try {
224308
var newRelease = opt.argv[0];
225-
if (newRelease === undefined)
226-
{
309+
if (newRelease === undefined) {
227310
console.log('Error: You must supply a version');
228311
process.exit(-1);
229312
}
@@ -242,4 +325,4 @@ async function main()
242325
}
243326
}
244327

245-
main();
328+
main();

0 commit comments

Comments
 (0)