Skip to content

Commit ddca2d9

Browse files
Add release notes automatization (#365)
1 parent 5d14692 commit ddca2d9

File tree

5 files changed

+709
-1
lines changed

5 files changed

+709
-1
lines changed

azure-pipelines.yml

+13
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,16 @@ stages:
102102
publishRegistry: useExternalRegistry
103103
publishEndpoint: NPM-Automation-Token
104104
continueOnError: true
105+
106+
- script: npm install
107+
displayName: npm install
108+
continueOnError: true
109+
condition: and(succeeded(), eq(variables.isMaster, true))
110+
111+
- script: node ./ci/create-release-notes.js
112+
continueOnError: true
113+
condition: and(succeeded(), eq(variables.isMaster, true))
114+
env:
115+
GH_TOKEN: $(githubToken)
116+
branch: $(Build.SourceBranchName)
117+
displayName: Create Release

ci/create-release-notes.js

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
const path = require('path')
2+
const fs = require('fs');
3+
4+
const { Octokit } = require('@octokit/rest');
5+
6+
const util = require('./utils');
7+
const basePath = path.join(__dirname, '..');
8+
9+
const token = process.env['GH_TOKEN'];
10+
const branch = process.env['branch'];
11+
12+
if (!token) {
13+
throw new util.CreateReleaseError('GH_TOKEN is not defined');
14+
}
15+
16+
if (!branch) {
17+
throw new util.CreateReleaseError('branch is not defined');
18+
}
19+
20+
const octokit = new Octokit({ auth: token });
21+
22+
const OWNER = 'microsoft';
23+
const REPO = 'typed-rest-client';
24+
25+
/**
26+
* The function looks for the date of the commit where the package version was bumped
27+
* @param {String} package - name of the package
28+
*/
29+
async function getPreviousReleaseDate() {
30+
const packagePath = path.join(basePath, 'package.json');
31+
const verRegExp = /"version":/;
32+
33+
function getHashFromVersion(verRegExp, ignoreHash) {
34+
let blameResult = ''
35+
if (ignoreHash) {
36+
blameResult = util.run(`git blame -w --ignore-rev ${ignoreHash} -- ${packagePath}`);
37+
} else {
38+
blameResult = util.run(`git blame -w -- ${packagePath}`);
39+
}
40+
const blameLines = blameResult.split('\n');
41+
const blameLine = blameLines.find(line => verRegExp.test(line));
42+
const commitHash = blameLine.split(' ')[0];
43+
return commitHash;
44+
}
45+
46+
const currentHash = getHashFromVersion(verRegExp);
47+
console.log(`Current version change is ${currentHash}`);
48+
const prevHash = getHashFromVersion(verRegExp, currentHash);
49+
console.log(`Previous version change is ${prevHash}`);
50+
51+
const date = await getPRDateFromCommit(prevHash);
52+
console.log(`Previous version change date is ${date}`);
53+
return date;
54+
}
55+
56+
57+
/**
58+
* Function to get the PR date from the commit hash
59+
* @param {string} sha1 - commit hash
60+
* @returns {Promise<string>} - date as a string with merged PR
61+
*/
62+
async function getPRDateFromCommit(sha1) {
63+
const response = await octokit.request('GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls', {
64+
owner: OWNER,
65+
repo: REPO,
66+
commit_sha: sha1,
67+
headers: {
68+
'X-GitHub-Api-Version': '2022-11-28'
69+
}
70+
});
71+
72+
if (!response.data.length) {
73+
throw new Error(`No PRs found for commit ${sha1}`);
74+
}
75+
76+
return response.data[0].merged_at;
77+
}
78+
79+
/**
80+
* Function to get the PR from the branch started from date
81+
* @param {string} branch - Branch to check for PRs
82+
* @param {string} date - Date since which to check for PRs
83+
* @returns {Promise<*>} - PRs merged since date
84+
*/
85+
async function getPRsFromDate(branch, date) {
86+
const PRs = [];
87+
let page = 1;
88+
try {
89+
while (true) {
90+
const results = await octokit.search.issuesAndPullRequests({
91+
q: `type:pr+is:merged+repo:${OWNER}/${REPO}+base:${branch}+merged:>${date}`,
92+
order: 'asc',
93+
sort: 'created',
94+
per_page: 100,
95+
page
96+
});
97+
98+
page++;
99+
if (results.data.items.length == 0) break;
100+
101+
PRs.push(...results.data.items);
102+
}
103+
104+
return PRs;
105+
} catch (e) {
106+
throw new Error(e.message);
107+
}
108+
}
109+
110+
/**
111+
* Function that create a release notes + tag for the new release
112+
* @param {string} releaseNotes - Release notes for the new release
113+
* @param {string} version - Version of the new release
114+
* @param {string} releaseBranch - Branch to create the release on
115+
*/
116+
async function createRelease(releaseNotes, version, releaseBranch) {
117+
const name = `Release v${version}`;
118+
const tagName = `v${version}`;
119+
console.log(`Creating release ${tagName} on ${releaseBranch}`);
120+
121+
const newRelease = await octokit.repos.createRelease({
122+
owner: OWNER,
123+
repo: REPO,
124+
tag_name: tagName,
125+
name: name,
126+
body: releaseNotes,
127+
target_commitish: releaseBranch,
128+
generate_release_notes: false
129+
});
130+
131+
console.log(`Release ${tagName} created`);
132+
console.log(`Release URL: ${newRelease.data.html_url}`);
133+
}
134+
135+
/**
136+
* Function to verify that the new release tag is valid.
137+
* @param {string} newRelease - Sprint version of the checked release
138+
* @returns {Promise<boolean>} - true - release exists, false - release does not exist
139+
*/
140+
async function isReleaseTagExists(version) {
141+
try {
142+
const tagName = `v${version}`;
143+
await octokit.repos.getReleaseByTag({
144+
owner: OWNER,
145+
repo: REPO,
146+
tag: tagName
147+
});
148+
149+
return true;
150+
} catch (e) {
151+
return false
152+
}
153+
}
154+
155+
156+
async function main(branch) {
157+
try {
158+
const version = util.getCurrentPackageVersion();
159+
const isReleaseExists = await isReleaseTagExists(version);
160+
if (isReleaseExists) {
161+
console.log(`Release v${version} already exists`);
162+
return;
163+
}
164+
165+
const date = await getPreviousReleaseDate();
166+
const data = await getPRsFromDate(branch, date);
167+
console.log(`Found ${data.length} PRs`);
168+
169+
const changes = util.getChangesFromPRs(data);
170+
if (!changes.length) {
171+
console.log(`No changes found for ${branch}`);
172+
return;
173+
}
174+
175+
const releaseNotes = changes.join('\n');
176+
await createRelease(releaseNotes, version, branch);
177+
} catch (e) {
178+
throw new util.CreateReleaseError(e.message);
179+
}
180+
}
181+
182+
main('master');

ci/utils.js

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
var fs = require('fs');
2+
var ncp = require('child_process');
3+
var path = require('path');
4+
var process = require('process');
5+
var shell = require('shelljs');
6+
const { exception } = require('console');
7+
8+
/**
9+
* Function to run command line via child_process.execSync
10+
* @param {*} cl Command line to run
11+
* @param {*} inheritStreams - Inherit/pipe stdio streams
12+
* @param {*} noHeader - Don't print command line header
13+
* @returns
14+
*/
15+
var run = function (cl, inheritStreams, noHeader) {
16+
if (!noHeader) {
17+
console.log();
18+
console.log('> ' + cl);
19+
}
20+
21+
var options = {
22+
stdio: inheritStreams ? 'inherit' : 'pipe'
23+
};
24+
var rc = 0;
25+
var output;
26+
try {
27+
output = ncp.execSync(cl, options);
28+
}
29+
catch (err) {
30+
if (!inheritStreams) {
31+
console.error(err.output ? err.output.toString() : err.message);
32+
}
33+
34+
throw new Error(`Command '${cl}' failed`)
35+
}
36+
37+
return (output || '').toString().trim();
38+
}
39+
exports.run = run;
40+
41+
class CreateReleaseError extends Error {
42+
constructor(message) {
43+
super(message);
44+
this.name = 'CreateReleaseError';
45+
Error.captureStackTrace(this, CreateReleaseError)
46+
}
47+
}
48+
49+
exports.CreateReleaseError = CreateReleaseError;
50+
/**
51+
* Function to form task changes from PRs
52+
* @param {Array<object>} PRs - PRs to get the release notes for
53+
* @returns {Object} - Object containing the task changes where key is a task and values - changes for the task
54+
*/
55+
function getChangesFromPRs(PRs) {
56+
const changes = [];
57+
PRs.forEach(PR => {
58+
59+
const closedDate = PR.pull_request.merged_at;
60+
const date = new Date(closedDate).toISOString().split('T')[0];
61+
changes.push(` - ${PR.title} (#${PR.number}) (${date})`);
62+
});
63+
64+
return changes;
65+
}
66+
exports.getChangesFromPRs = getChangesFromPRs;
67+
68+
/**
69+
* Function to get current version of the package
70+
* @param {String} package - Package name
71+
* @returns {String} - version of the package
72+
**/
73+
74+
function getCurrentPackageVersion() {
75+
const packagePath = path.join(__dirname, '..', 'package.json');
76+
if (!fs.existsSync(packagePath)) {
77+
throw new CreateReleaseError(`package.json not found.`)
78+
}
79+
const packageJson = require(packagePath);
80+
return packageJson.version;
81+
}
82+
exports.getCurrentPackageVersion = getCurrentPackageVersion;

0 commit comments

Comments
 (0)