AutorÃa | Ultima modificación | Ver Log |
#!/usr/bin/env node// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>.import chalk from 'chalk';import { getAllComponents } from './components.mjs';import { getCombinedNotesByComponent, deleteAllNotes } from './note.mjs';import { getNoteName } from './noteTypes.mjs';import { writeFile, readFile, unlink } from 'fs/promises';import { join as joinPath } from 'path';import logger from './logger.mjs';import { getCurrentVersion } from './helpers.mjs';/*** Helper to fetch the current notes from a file.** @param {string} file* @returns {Promise<string>}*/const getCurrentNotes = async (file) => {try {return await readFile(file, 'utf8');} catch (error) {return null;}}/*** Update the UPGRADING.md file.** @param {string} upgradeNotes* @param {Object} options* @param {boolean} options.deleteNotes* @returns {Promise<void>}*/const updateUpgradeNotes = async (upgradeNotes, options) => {const fileName = 'UPGRADING.md';// Write the notes to a file.logger.info(`Writing notes to ${chalk.underline(chalk.bold(fileName))}`);// Prepend to the existing file.const existingContent = await getCurrentNotes(fileName);if (existingContent) {await writeFile(fileName, getUpdatedNotes(existingContent, upgradeNotes));} else {// This should not normally happen.await writeFile(fileName, upgradeNotes);}if (options.deleteNotes) {logger.warn(`>>> Deleting all notes <<<`)// Delete the notes.deleteAllNotes();}};/*** Create the current summary notes.** @param {string} upgradeNotes* @returns {Promise<void>}*/const createCurrentSummary = async (upgradeNotes) => {const fileName = 'UPGRADING-CURRENT.md';const notes = `# Moodle upgrade notes\n\n${upgradeNotes}`;await writeFile(fileName, notes);logger.info(`Running upgrade notes written to ${chalk.underline(chalk.bold(fileName))}`);};/*** Get the indexes of the lines that contain the version headings.** @param {array<string>} lines* @returns {array<object>}*/const getVersionLineIndexes = (lines) => {const h2Indexes = [];lines.forEach((line, index) => {const matches = line.match(/^##\s(?<version>.*)$/);if (matches) {h2Indexes.push({index,line,version: matches.groups.version,});}});return h2Indexes;};/*** Find the index of the Unreleased heading.** @param {array<object>} versionHeadings* @returns {number}*/const findUnreleasedHeadingIndex = (versionHeadings) => versionHeadings.findIndex((heading) => {if (heading.version === 'Unreleased') {// Used if version cannot be guessed.return true;}if (heading.version.endsWith('+')) {// Weekly release for a stable branch.return true;}if (heading.version.match(/beta|rc\d/)) {// Beta and RC rolls are treated as weeklies.return true;}if (heading.version.endsWith('dev')) {// Development version.return true;}return false;});/*** Get the before and after content, to facilitate replacing any existing Unreleased notes.** @param {array<string>} lines* @returns {Object} {beforeContent: string, afterContent: string}*/const getBeforeAndAfterContent = (lines) => {const existingLines = lines.split('\n');const versionHeadings = getVersionLineIndexes(existingLines);if (versionHeadings.length > 0) {const unreleasedHeadingIndex = findUnreleasedHeadingIndex(versionHeadings);if (unreleasedHeadingIndex !== -1) {const beforeContent = existingLines.slice(0, versionHeadings[unreleasedHeadingIndex].index).join('\n');if (versionHeadings.length > unreleasedHeadingIndex + 1) {const afterContent = existingLines.slice(versionHeadings[unreleasedHeadingIndex + 1].index).join('\n');return {beforeContent,afterContent,};}return {beforeContent,afterContent: '',};}return {beforeContent: existingLines.slice(0, versionHeadings[0].index).join('\n'),afterContent: existingLines.slice(versionHeadings[0].index).join('\n'),};}return {beforeContent: existingLines.join('\n'),afterContent: '',}};/*** Get the notes for the component.** @param {string} types* @param {Number} headingLevel* @returns {string}*/const getNotesForComponent = (types, headingLevel) => {let upgradeNotes = '';Object.entries(types).forEach(([type, notes]) => {upgradeNotes += '#'.repeat(headingLevel);upgradeNotes += ` ${getNoteName(type)}\n\n`;notes.forEach(({ message, issueNumber }) => {// Split the message into lines, removing empty lines.const messageLines = message.split('\n')// Remove empty lines between tables, and list entries, but not after lists..filter((line, index, lines) => {if (line.trim().length === 0) {// This line is empty.// If it's the first line in the file, remove it.if (index === 0) {return false;}// This is the last line in the file, remove it.if (index === lines.length - 1) {return false;}// If the previous line relates to a table, remove this line.if (lines[index - 1].match(/^\s*\|/)) {return false;}// If the next line is also empty, do not remove this line.if (lines[index + 1].trim().length === 0) {return true;}// Do not remove the line if the previous line was a list item.if (lines[index - 1].match(/^\s*[-*]\s/)) {return true;}if (lines[index - 1].match(/^\s*\d+\.\s/)) {return true;}// Preserve all other empty lines by default.return true;}// Keep any line which has content.return true;});const firstLine = messageLines.shift().trim();upgradeNotes += `- ${firstLine}\n`;messageLines.forEach((line) => {upgradeNotes += ` ${line}`.trimEnd() + `\n`;});upgradeNotes += `\n For more information see [${issueNumber}](https://tracker.moodle.org/browse/${issueNumber})\n`;});upgradeNotes += '\n';});return upgradeNotes;};/*** Get the updated notes mixed with existing content.** @param {string} existingContent* @param {string} upgradeNotes*/const getUpdatedNotes = (existingContent, upgradeNotes) => {const { beforeContent, afterContent } = getBeforeAndAfterContent(existingContent);const newContent = `${beforeContent}\n${upgradeNotes}\n${afterContent}`.split('\n').filter((line, index, lines) => {if (line === '' && lines[index - 1] === '') {// Remove multiple consecutive empty lines.return false;}return true;}).join('\n');return newContent;};/*** Update the notes for each component.*/const updateComponentNotes = (notes,version,notesFileName = 'UPGRADING.md',removeEmpty = false,) => {return getAllComponents().map(async (component) => {logger.verbose(`Updating notes for ${component.name} into ${component.path}`);const fileName = joinPath(component.path, notesFileName);const existingContent = await getCurrentNotes(fileName);if (!existingContent) {if (!notes[component.value]) {// No existing notes, and no new notes to add.return;}} else {if (!notes[component.value]) {// There is existing content, but nothing to add.if (removeEmpty) {logger.verbose(`Removing empty notes file ${fileName}`);await unlink(fileName);}return;}}const componentNotes = notes[component.value];let upgradeNotes = `## ${version}\n\n`;upgradeNotes += getNotesForComponent(componentNotes, 3);if (existingContent) {await writeFile(fileName, getUpdatedNotes(existingContent, upgradeNotes));} else {await writeFile(fileName,`# ${component.name} Upgrade notes\n\n${upgradeNotes}`,);}});}/*** Generate the upgrade notes for a new release.** @param {string|undefined} version* @param {Object} options* @param {boolean} options.generateUpgradeNotes* @param {boolean} options.deleteNotes* @returns {Promise<void>}*/export default async (version, options = {}) => {const notes = await getCombinedNotesByComponent();if (Object.keys(notes).length === 0) {logger.warn('No notes to generate');return;}if (!version) {version = await getCurrentVersion();}// Generate the upgrade notes for this release.// We have// - a title with the release name// - the change types// - which contain the components// - which document each changelet upgradeNotes = `## ${version}\n\n`;Object.entries(notes).forEach(([component, types]) => {upgradeNotes += `### ${component}\n\n`;upgradeNotes += getNotesForComponent(types, 4);});await Promise.all([createCurrentSummary(upgradeNotes),...updateComponentNotes(notes, version, 'UPGRADING-CURRENT.md', true),]);if (options.generateUpgradeNotes) {await Promise.all(updateComponentNotes(notes, version));await updateUpgradeNotes(upgradeNotes, options);}};