Skip to content

Commit 4adff37

Browse files
authored
chore(NODE-5382): backport release automation scripts (#3747)
1 parent 2d028af commit 4adff37

File tree

5 files changed

+225
-0
lines changed

5 files changed

+225
-0
lines changed

.github/actions/setup/action.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Setup
2+
description: 'Installs node, driver dependencies, and builds source'
3+
4+
runs:
5+
using: composite
6+
steps:
7+
- uses: actions/setup-node@v3
8+
with:
9+
node-version: 'lts/*'
10+
cache: 'npm'
11+
registry-url: 'https://registry.npmjs.org'
12+
- run: npm install -g npm@latest
13+
shell: bash
14+
- run: npm clean-install
15+
shell: bash

.github/scripts/highlights.mjs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// @ts-check
2+
import * as process from 'node:process';
3+
import { Octokit } from '@octokit/core';
4+
import { output } from './util.mjs';
5+
6+
const {
7+
GITHUB_TOKEN = '',
8+
PR_LIST = '',
9+
owner = 'mongodb',
10+
repo = 'node-mongodb-native'
11+
} = process.env;
12+
if (GITHUB_TOKEN === '') throw new Error('GITHUB_TOKEN cannot be empty');
13+
14+
const octokit = new Octokit({
15+
auth: GITHUB_TOKEN,
16+
log: {
17+
debug: msg => console.error('Octokit.debug', msg),
18+
info: msg => console.error('Octokit.info', msg),
19+
warn: msg => console.error('Octokit.warn', msg),
20+
error: msg => console.error('Octokit.error', msg)
21+
}
22+
});
23+
24+
const prs = PR_LIST.split(',').map(pr => {
25+
const prNum = Number(pr);
26+
if (Number.isNaN(prNum))
27+
throw Error(`expected PR number list: ${PR_LIST}, offending entry: ${pr}`);
28+
return prNum;
29+
});
30+
31+
/** @param {number} pull_number */
32+
async function getPullRequestContent(pull_number) {
33+
const startIndicator = 'RELEASE_HIGHLIGHT_START -->';
34+
const endIndicator = '<!-- RELEASE_HIGHLIGHT_END';
35+
36+
let body;
37+
try {
38+
const res = await octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', {
39+
owner,
40+
repo,
41+
pull_number,
42+
headers: { 'X-GitHub-Api-Version': '2022-11-28' }
43+
});
44+
body = res.data.body;
45+
} catch (error) {
46+
console.log(`Could not get PR ${pull_number}, skipping. ${error.status}`);
47+
return '';
48+
}
49+
50+
if (body == null || !(body.includes(startIndicator) && body.includes(endIndicator))) {
51+
console.log(`PR #${pull_number} has no highlight`);
52+
return '';
53+
}
54+
55+
const start = body.indexOf('### ', body.indexOf(startIndicator));
56+
const end = body.indexOf(endIndicator);
57+
const highlightSection = body.slice(start, end).trim();
58+
59+
console.log(`PR #${pull_number} has a highlight ${highlightSection.length} characters long`);
60+
return highlightSection;
61+
}
62+
63+
/** @param {number[]} prs */
64+
async function pullRequestHighlights(prs) {
65+
const highlights = [];
66+
for (const pr of prs) {
67+
const content = await getPullRequestContent(pr);
68+
highlights.push(content);
69+
}
70+
if (!highlights.length) return '';
71+
72+
highlights.unshift('## Release Notes\n\n');
73+
return highlights.join('\n\n');
74+
}
75+
76+
console.log('List of PRs to collect highlights from:', prs);
77+
const highlights = await pullRequestHighlights(prs);
78+
79+
await output('highlights', JSON.stringify({ highlights }));

.github/scripts/pr_list.mjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// @ts-check
2+
import * as url from 'node:url';
3+
import * as fs from 'node:fs/promises';
4+
import * as path from 'node:path';
5+
import { getCurrentHistorySection, output } from './util.mjs';
6+
7+
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
8+
const historyFilePath = path.join(__dirname, '..', '..', 'HISTORY.md');
9+
10+
/**
11+
* @param {string} history
12+
* @returns {string[]}
13+
*/
14+
function parsePRList(history) {
15+
const prRegexp = /node-mongodb-native\/issues\/(?<prNum>\d+)\)/iu;
16+
return history
17+
.split('\n')
18+
.map(line => prRegexp.exec(line)?.groups?.prNum ?? '')
19+
.filter(prNum => prNum !== '');
20+
}
21+
22+
const historyContents = await fs.readFile(historyFilePath, { encoding: 'utf8' });
23+
24+
const currentHistorySection = getCurrentHistorySection(historyContents);
25+
26+
const prs = parsePRList(currentHistorySection);
27+
28+
await output('pr_list', prs.join(','));

.github/scripts/release_notes.mjs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//@ts-check
2+
import * as url from 'node:url';
3+
import * as fs from 'node:fs/promises';
4+
import * as path from 'node:path';
5+
import * as process from 'node:process';
6+
import * as semver from 'semver';
7+
import { getCurrentHistorySection, output } from './util.mjs';
8+
9+
const { HIGHLIGHTS = '' } = process.env;
10+
if (HIGHLIGHTS === '') throw new Error('HIGHLIGHTS cannot be empty');
11+
12+
const { highlights } = JSON.parse(HIGHLIGHTS);
13+
14+
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
15+
const historyFilePath = path.join(__dirname, '..', '..', 'HISTORY.md');
16+
const packageFilePath = path.join(__dirname, '..', '..', 'package.json');
17+
18+
const historyContents = await fs.readFile(historyFilePath, { encoding: 'utf8' });
19+
20+
const currentHistorySection = getCurrentHistorySection(historyContents);
21+
22+
const version = semver.parse(
23+
JSON.parse(await fs.readFile(packageFilePath, { encoding: 'utf8' })).version
24+
);
25+
if (version == null) throw new Error(`could not create semver from package.json`);
26+
27+
console.log('\n\n--- history entry ---\n\n', currentHistorySection);
28+
29+
const currentHistorySectionLines = currentHistorySection.split('\n');
30+
const header = currentHistorySectionLines[0];
31+
const history = currentHistorySectionLines.slice(1).join('\n').trim();
32+
33+
const releaseNotes = `${header}
34+
35+
The MongoDB Node.js team is pleased to announce version ${version.version} of the \`mongodb\` package!
36+
37+
${highlights}
38+
${history}
39+
## Documentation
40+
41+
* [Reference](https://docs.mongodb.com/drivers/node/current/)
42+
* [API](https://mongodb.github.io/node-mongodb-native/${version.major}.${version.minor}/)
43+
* [Changelog](https://github.com/mongodb/node-mongodb-native/blob/v${version.version}/HISTORY.md)
44+
45+
We invite you to try the \`mongodb\` library immediately, and report any issues to the [NODE project](https://jira.mongodb.org/projects/NODE).
46+
`;
47+
48+
const releaseNotesPath = path.join(process.cwd(), 'release_notes.md');
49+
50+
await fs.writeFile(
51+
releaseNotesPath,
52+
`:seedling: A new release!\n---\n${releaseNotes}\n---\n`,
53+
{ encoding:'utf8' }
54+
);
55+
56+
await output('release_notes_path', releaseNotesPath)

.github/scripts/util.mjs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// @ts-check
2+
import * as process from 'node:process';
3+
import * as fs from 'node:fs/promises';
4+
5+
export async function output(key, value) {
6+
const { GITHUB_OUTPUT = '' } = process.env;
7+
const output = `${key}=${value}\n`;
8+
console.log('outputting:', output);
9+
10+
if (GITHUB_OUTPUT.length === 0) {
11+
// This is always defined in Github actions, and if it is not for some reason, tasks that follow will fail.
12+
// For local testing it's convenient to see what scripts would output without requiring the variable to be defined.
13+
console.log('GITHUB_OUTPUT not defined, printing only');
14+
return;
15+
}
16+
17+
const outputFile = await fs.open(GITHUB_OUTPUT, 'a');
18+
await outputFile.appendFile(output, { encoding: 'utf8' });
19+
await outputFile.close();
20+
}
21+
22+
/**
23+
* @param {string} historyContents
24+
* @returns {string}
25+
*/
26+
export function getCurrentHistorySection(historyContents) {
27+
/** Markdown version header */
28+
const VERSION_HEADER = /^#.+\(\d{4}-\d{2}-\d{2}\)$/g;
29+
30+
const historyLines = historyContents.split('\n');
31+
32+
// Search for the line with the first version header, this will be the one we're releasing
33+
const headerLineIndex = historyLines.findIndex(line => VERSION_HEADER.test(line));
34+
if (headerLineIndex < 0) throw new Error('Could not find any version header');
35+
36+
console.log('Found markdown header current release', headerLineIndex, ':', historyLines[headerLineIndex]);
37+
38+
// Search lines starting after the first header, and add back the offset we sliced at
39+
const nextHeaderLineIndex = historyLines
40+
.slice(headerLineIndex + 1)
41+
.findIndex(line => VERSION_HEADER.test(line)) + headerLineIndex + 1;
42+
if (nextHeaderLineIndex < 0) throw new Error(`Could not find previous version header, searched ${headerLineIndex + 1}`);
43+
44+
console.log('Found markdown header previous release', nextHeaderLineIndex, ':', historyLines[nextHeaderLineIndex]);
45+
46+
return historyLines.slice(headerLineIndex, nextHeaderLineIndex).join('\n');
47+
}

0 commit comments

Comments
 (0)