#!/usr/bin/env node /** * What's this?? It will help you create release blog * posts so you won't have to do the tedious work * of stitching together data from changelog, shasums etc, * but get a more or less complete release blog ready to go. * * Usage: $ node index.mjs [version] * * If the version argument is omitted, the latest version number * will be picked from https://nodejs.org/dist/index.json. * * It'll create a file with the blog post content * into ../../pages/en/blog/release/vX.md ready for you to commit * or possibly edit by hand before committing. * * Happy releasing! */ 'use strict'; import { existsSync, readFileSync } from 'node:fs'; import { writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import handlebars from 'handlebars'; import { format } from 'prettier'; import { downloadsTable } from './downloadsTable.mjs'; import prettierConfig from '../../.prettierrc.json' assert { type: 'json' }; import { getRelativePath } from '../../next.helpers.mjs'; const URLS = { NODE_DIST_JSON: 'https://nodejs.org/dist/index.json', GITHUB_PROFILE: author => `https://api.github.com/users/${author}`, NODE_CHANGELOG_MD: releaseLine => `https://raw.githubusercontent.com/nodejs/node/main/doc/changelogs/CHANGELOG_V${releaseLine}.md`, NODE_SHASUM: version => `https://nodejs.org/dist/v${version}/SHASUMS256.txt.asc`, }; const ERRORS = { NO_VERSION_PROVIDED: new Error('No version provided'), RELEASE_EXISTS: version => new Error(`Release post for ${version} already exists!`), NO_AUTHOR_FOUND: version => new Error(`Couldn't find @author of ${version} release :(`), NO_VERSION_POLICY: version => new Error(`Could not find version policy of ${version} in its changelog`), NO_CHANGELOG_FOUND: version => new Error(`Couldn't find matching changelog for ${version}`), INVALID_STATUS_CODE: (url, status) => new Error(`Invalid status (!= 200) while retrieving ${url}: ${status}`), FAILED_FILE_FORMATTING: reason => new Error(`Failed to format Release post: Reason: ${reason}`), FAILED_FILE_CREATION: reason => new Error(`Failed to write Release post: Reason: ${reason}`), }; const ARGS = { CURRENT_PATH: process.argv[1], SPECIFIC_VERSION: process.argv[2] && process.argv[2].replace('--force', ''), SHOULD_FORCE: (process.argv[3] || process.argv[2]) === '--force', }; // this allows us to get the current module working directory const __dirname = getRelativePath(import.meta.url); const request = options => { return fetch(options.url, options).then(resp => { if (resp.status !== 200) { throw ERRORS.INVALID_STATUS_CODE(options.url, resp.status); } return options.json ? resp.json() : resp.text(); }); }; const explicitVersion = version => new Promise((resolve, reject) => version && version.length > 0 ? resolve(version) : reject(ERRORS.NO_VERSION_PROVIDED) ); const findLatestVersion = () => request({ url: URLS.NODE_DIST_JSON, json: true }) .then(versions => versions.length && versions[0]) .then(({ version }) => version.substr(1)); const fetchDocs = version => { const blogPostPieces = [ fetchChangelogBody(version), fetchAuthor(version), fetchVersionPolicy(version), fetchShasums(version), verifyDownloads(version), ]; return Promise.all(blogPostPieces).then( ([changelog, author, versionPolicy, shasums, files]) => ({ version, changelog, author, versionPolicy, shasums, files, }) ); }; const fetchAuthor = version => { return fetchChangelog(version) .then(section => findAuthorLogin(version, section)) .then(author => request({ url: URLS.GITHUB_PROFILE(author), json: true })) .then(githubRes => githubRes.name); }; const fetchChangelog = version => { const parts = version.split('.'); const releaseLine = parts[0] === '0' ? parts.slice(0, 2).join('') : parts[0]; return request({ url: URLS.NODE_CHANGELOG_MD(releaseLine) }).then(data => { // matches a complete release section const rxSection = new RegExp( `\\n([\\s\\S]+?)(?:\\n