From 19e824932d8dea82c08970bd21bc832dca4c7a3a Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 8 Oct 2024 11:18:06 +0800 Subject: [PATCH 1/3] [scripts] Add rudimentary version helper --- scripts/version.mjs | 465 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 465 insertions(+) create mode 100644 scripts/version.mjs diff --git a/scripts/version.mjs b/scripts/version.mjs new file mode 100644 index 0000000000..3eb2e7e35f --- /dev/null +++ b/scripts/version.mjs @@ -0,0 +1,465 @@ +#!/usr/bin/env node +/** + * Copyright (c) Moodle Pty Ltd. + * + * 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 . + */ + +/* eslint-disable import/no-extraneous-dependencies */ + +import { program } from 'commander'; +import { readFile, writeFile } from 'fs/promises'; +import inquirer from 'inquirer'; + +const versionData = JSON.parse(await readFile('./data/versions.json')); + +const getMajorVersionFromData = (majorVersion) => versionData.versions.find((version) => version.name === majorVersion); +const getMajorVersionIndexFromData = (majorVersion) => versionData.versions.findIndex( + (version) => version.name === majorVersion, +); +const getMajorVersionNameFromMinor = (minorVersion) => minorVersion.split('.').slice(0, 2).join('.'); +const updateMajorVersionWithData = (majorVersion, data) => { + const index = getMajorVersionIndexFromData(majorVersion); + versionData.versions[index] = data; +}; +const updateMinorVersionWithData = (majorVersion, minorVersion, data) => { + const majorVersionData = getMajorVersionFromData(majorVersion); + const minorVersionIndex = majorVersionData.releases.findIndex((release) => release.name === minorVersion); + majorVersionData.releases[minorVersionIndex] = data; + updateMajorVersionWithData(majorVersion, majorVersionData); +}; + +const persistVersionData = async () => { + await writeFile('./data/versions.json', JSON.stringify(versionData, null, 4)); +}; + +const getFormatter = (options = {}) => new Intl.DateTimeFormat('en-AU', { + timezone: 'Australia/Perth', + weekday: 'short', + month: 'short', + year: 'numeric', + day: 'numeric', + ...options, +}); + +const getDateParts = (date, options = {}) => { + const formattedDate = getFormatter(options).formatToParts(date); + + return Object.fromEntries(formattedDate.map(({ type, value }) => [type, value])); +}; + +const getFormattedDate = (date) => { + const dateParts = getDateParts(date); + + return `${dateParts.weekday} ${dateParts.day} ${dateParts.month} ${dateParts.year}`; +}; + +const getReleaseDateFromDate = (date) => { + const dateParts = getDateParts(date, { + month: '2-digit', + day: '2-digit', + }); + + return `${dateParts.year}${dateParts.month}${dateParts.day}00`; +}; + +const getDateFormattedForVersionFile = (date) => { + const dateParts = getDateParts(date, { + month: 'long', + day: 'numeric', + }); + + return `${dateParts.day} ${dateParts.month} ${dateParts.year}`; +}; + +const getThreeDigitVersionFromName = (name) => { + const [major, minor] = name.split('.'); + + if (Number(minor) < 10) { + return `${major}0${minor}`; + } + + return `${major}${minor}`; +}; + +const getMajorVersion = async (defaultVersion = null) => { + const { major } = await inquirer.prompt({ + type: 'input', + name: 'major', + message: 'What version do you want to add a release to?', + default: defaultVersion, + validate: (input) => (input.match(/^\d+\.\d+$/) ? true : 'Version must be a number'), + }); + + return major; +}; + +const getReleaseDate = async (defaultDate = null) => ( + await inquirer.prompt({ + type: 'input', + name: 'releaseDate', + message: 'What is the expected release date?', + default: defaultDate, + validate: (input) => { + const parsedDate = new Date(input); + + if (Number.isNaN(parsedDate.valueOf())) { + return 'Invalid date. Dates should be in a valid JS Date format.'; + } + + return true; + }, + filter: (input) => new Date(input), + }) +).releaseDate; + +const getFreezeDate = async (releaseDate) => { + const { hasFreezeDate } = await inquirer.prompt({ + name: 'hasFreezeDate', + type: 'confirm', + message: 'Do you want to set a code freeze date?', + default: true, + }); + + if (!hasFreezeDate) { + return null; + } + + const getSuggestedDate = () => getFormattedDate(new Date( + new Date(releaseDate) + .setDate(releaseDate.getDate() - (6 * 7)), + )); + + const { theDate } = await inquirer.prompt({ + type: 'input', + name: 'theDate', + message: 'What is the code freeze date?', + default: getSuggestedDate(), + validate: (input) => { + const parsedDate = new Date(input); + + if (Number.isNaN(parsedDate.valueOf())) { + return 'Invalid date. Dates should be in a valid JS Date format.'; + } + + if (parsedDate > releaseDate) { + return 'Code freeze date must be before the release date'; + } + + return true; + }, + filter: (input) => new Date(input), + }); + + return theDate; +}; + +const getGeneralSupportDate = async (releaseDate) => { + const getSuggestedDate = () => { + const supportLength = 12; + let calculatedDate; + calculatedDate = new Date(new Date(releaseDate).setMonth(releaseDate.getMonth() + supportLength)); + calculatedDate = new Date(calculatedDate.setDate(calculatedDate.getDate() - 1)); + return getFormattedDate(calculatedDate); + }; + + const { theDate } = await inquirer.prompt({ + type: 'input', + name: 'theDate', + message: 'What is the general support end date?', + default: getSuggestedDate(), + validate: (input) => { + const parsedDate = new Date(input); + + if (Number.isNaN(parsedDate.valueOf())) { + return 'Invalid date. Dates should be in ISO format.'; + } + + if (parsedDate < releaseDate) { + return 'General Support must be after the release date'; + } + + return true; + }, + filter: (input) => new Date(input), + }); + + return theDate; +}; + +const getSecurityEndDate = async (releaseDate, generalSupportEndDate, isLTS) => { + const getSuggestedDate = () => { + const supportLength = isLTS ? 36 : 18; + let calculatedDate; + calculatedDate = new Date(new Date(releaseDate).setMonth(releaseDate.getMonth() + supportLength)); + calculatedDate = new Date(calculatedDate.setDate(calculatedDate.getDate() - 1)); + return getFormattedDate(calculatedDate); + }; + + const { theDate } = await inquirer.prompt({ + type: 'input', + name: 'theDate', + message: 'What is the security support end date?', + default: getSuggestedDate(), + validate: (input) => { + const parsedDate = new Date(input); + + if (Number.isNaN(parsedDate.valueOf())) { + return 'Invalid date. Dates should be in ISO format.'; + } + + if (parsedDate < generalSupportEndDate) { + return 'Security support must must be after the general support end date'; + } + + return true; + }, + filter: (input) => new Date(input), + }); + + return theDate; +}; + +const getReleaseVersionForMajor = async (schema, releaseDate) => { + const { version } = await inquirer.prompt({ + type: 'input', + name: 'version', + message: 'What is the release version?', + default: getReleaseDateFromDate(releaseDate), + validate: (input) => { + if (input.match(/^\d{10}$/)) { + return true; + } + + return 'Version must be in the format YYYYMMDD00'; + }, + }); + + return version; +}; + +const getNotes = async () => { + const { addNotes } = await inquirer.prompt({ + type: 'confirm', + name: 'addNotes', + message: 'Do you want to add notes?', + default: false, + }); + + if (!addNotes) { + return null; + } + + const { standardNotes } = await inquirer.prompt({ + type: 'expand', + name: 'standardNotes', + message: 'Choose a standard note, or write your own', + choices: [{ + key: 'c', + name: 'Cancel (do not add any notes)', + value: null, + }, { + key: 's', + name: 'Unscheduled minor release', + }, { + key: 'e', + name: 'Support has ended', + }, { + key: 'n', + name: 'Custom', + value: false, + }], + }); + + if (standardNotes || standardNotes === null) { + return standardNotes; + } + + const { notes } = await inquirer.prompt({ + type: 'input', + name: 'notes', + message: 'Enter the notes', + }); + + return notes; +}; + +const addMinorRelease = async ( + confirmAddAnother = false, + suggestedReleaseDate = null, + versionList = [], +) => { + if (confirmAddAnother) { + const { addMinor } = await inquirer.prompt({ + type: 'confirm', + name: 'addMinor', + message: 'Do you want to add another minor release?', + default: false, + }); + if (!addMinor) { + return; + } + } + + const majorVersionName = await getMajorVersion(versionList.length ? versionList.pop() : null); + const thisVersionData = getMajorVersionFromData(majorVersionName); + + if (!thisVersionData) { + throw new Error(`Major version ${majorVersionName} does not exist`); + } + + const { releases } = thisVersionData; + const lastRelease = releases[releases.length - 1]; + const lastReleaseName = lastRelease.name.split('.'); + const nextReleaseName = `${majorVersionName}.${Number(lastReleaseName[2]) + 1}`; + const nextReleaseVersion = lastRelease.version + 1; + + const releaseDate = await getReleaseDate(suggestedReleaseDate); + + const schema = { + name: nextReleaseName, + releaseDate: getDateFormattedForVersionFile(releaseDate), + version: nextReleaseVersion, + releaseNoteUrl: false, + }; + + const notes = await getNotes(); + + if (notes) { + schema.notes = notes; + } + + console.log(`Adding the following schema for ${majorVersionName}`); + console.log(schema); + + thisVersionData.releases.push(schema); + updateMajorVersionWithData(majorVersionName, thisVersionData); + await persistVersionData(); + + addMinorRelease(!versionList.length, releaseDate, versionList); +}; + +const addMajorRelease = async ( + name, + minors = [], +) => { + console.log('------------------------------------------------------------------------'); + console.log(`Adding a new major version: ${name}`); + console.log('------------------------------------------------------------------------'); + console.log(''); + console.log('Notes:'); + console.log(''); + console.log('- All dates must be in the format DD Month YYYY, for example 24 Feb 2025'); + console.log('- Code freeze dates are typically 6 weeks before the release date.'); + console.log('- Regular releases have an 18-month support period.'); + console.log('- LTS releases have a 3-year support period.'); + console.log(''); + console.log('All end dates are on the release date of that major release.'); + console.log('------------------------------------------------------------------------'); + console.log(''); + if (!name.match(/^\d+\.\d+$/)) { + throw new Error('Major versions must be in the format X.Y'); + } + + if (versionData.versions[name]) { + throw new Error(`Version ${name} already exists`); + } + + const { isLTS } = await inquirer.prompt({ + type: 'confirm', + name: 'isLTS', + message: 'Is this an LTS release?', + default: false, + }); + + const releaseDate = await getReleaseDate(); + const codeFreezeDate = await getFreezeDate(releaseDate); + const generalSupportEndDate = await getGeneralSupportDate(releaseDate); + const securityEndDate = await getSecurityEndDate(releaseDate, generalSupportEndDate, isLTS); + + const schema = { + name, + releaseDate: getDateFormattedForVersionFile(releaseDate), + generalEndDate: getDateFormattedForVersionFile(generalSupportEndDate), + securityEndDate: getDateFormattedForVersionFile(securityEndDate), + isLTS, + }; + + if (codeFreezeDate) { + schema.codeFreezeDate = getDateFormattedForVersionFile(codeFreezeDate); + } + + const releaseVersion = await getReleaseVersionForMajor(schema, releaseDate); + + const threeDigitVersion = getThreeDigitVersionFromName(name); + schema.releases = [{ + name: `${name}.0`, + releaseDate: schema.releaseDate, + version: Number(releaseVersion), + upgradePath: `https://docs.moodle.org/${threeDigitVersion}/en/Upgrading`, + releaseNoteUrl: false, + }]; + + versionData.versions.unshift(schema); + + console.log(`Adding the following schema for ${name}`); + console.log(schema); + await persistVersionData(); + addMinorRelease(!minors.length, releaseDate, minors); +}; + +const releaseVersions = (versions) => { + for (const version of versions) { + const majorVersionName = getMajorVersionNameFromMinor(version); + const majorVersion = getMajorVersionFromData(majorVersionName); + + if (!majorVersion) { + throw new Error(`Version ${version} does not exist`); + } + + const minorVersionIndex = majorVersion.releases.findIndex((release) => release.name === version); + if (minorVersionIndex === -1) { + throw new Error(`Version ${version} does not exist`); + } + + const minorVersionData = majorVersion.releases[minorVersionIndex]; + delete minorVersionData.releaseNoteUrl; + updateMinorVersionWithData(majorVersionName, version, minorVersionData); + } + + persistVersionData(); +}; + +program + .name('Version Manager') + .description('CLI tooling to help manage version data for Moodle releases'); + +program + .command('major [majors...]') + .description( + 'Generate a new major version, and optionally a list of other major releases with upcoming minor releases', + ) + .action(addMajorRelease); + +program + .command('minors [majors...]') + .description('Generate new minor release versions for the supplied list of major releases') + .action((majors) => addMinorRelease(false, null, majors)); + +program + .command('release ') + .description('Mark the supplied minor releases as released by removing the releaseNoteUrl:false property') + .action(releaseVersions); + +program.parse(); From 7e54451cde857f4915fa224de2698c1f3bef3af5 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 8 Oct 2024 11:20:14 +0800 Subject: [PATCH 2/3] [data] Correct formatting of versions file --- data/versions.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/data/versions.json b/data/versions.json index 43fbcc7337..b94504790e 100644 --- a/data/versions.json +++ b/data/versions.json @@ -1497,7 +1497,6 @@ "name": "2.7.7", "releaseDate": "10 March 2015", "version": 2014051207 - }, { "name": "2.7.8", @@ -2097,17 +2096,17 @@ { "name": "1.9.8", "releaseDate": "25 March 2010", - "version": 2007101580.00 + "version": 2007101580 }, { "name": "1.9.9", "releaseDate": "8 June 2010", - "version": 2007101590.00 + "version": 2007101590 }, { "name": "1.9.10", "releaseDate": "25 October 2010", - "version": 2007101591.00 + "version": 2007101591 }, { "name": "1.9.11", @@ -2137,7 +2136,7 @@ { "name": "1.9.16", "releaseDate": "9 January 2012", - "version": 2007101591.10 + "version": 2007101591.1 }, { "name": "1.9.17", @@ -2152,7 +2151,7 @@ { "name": "1.9.19", "releaseDate": "9 July 2012", - "version": 2007101592.00, + "version": 2007101592, "notes": "Support has ended" } ] @@ -2229,4 +2228,4 @@ "isLTS": false } ] -} +} \ No newline at end of file From 833c1c9769185172242302a840c77049ef7347ee Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 8 Oct 2024 12:08:45 +0800 Subject: [PATCH 3/3] [data] Add next version release information Next minor versions are: - 4.1.15 - 9th December 2024 - 4.3.9 - 9th December 2024 - 4.4.5 - 9th December 2024 - 4.5.1 - 9th December 2024 Next major versions is: - 5.0.0 - 14th April 2025 --- data/versions.json | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/data/versions.json b/data/versions.json index b94504790e..c90c27b636 100644 --- a/data/versions.json +++ b/data/versions.json @@ -1,6 +1,23 @@ { "$schema": "../static/schema/versions.json", "versions": [ + { + "name": "5.0", + "releaseDate": "14 April 2025", + "generalEndDate": "20 April 2026", + "securityEndDate": "5 October 2026", + "isLTS": false, + "codeFreezeDate": "3 March 2025", + "releases": [ + { + "name": "5.0.0", + "releaseDate": "14 April 2025", + "version": 2025041400, + "upgradePath": "https://docs.moodle.org/500/en/Upgrading", + "releaseNoteUrl": false + } + ] + }, { "name": "4.5", "releaseDate": "7 October 2024", @@ -13,6 +30,12 @@ "releaseDate": "7 October 2024", "version": 2024100700, "upgradePath": "https://docs.moodle.org/405/en/Upgrading" + }, + { + "name": "4.5.1", + "releaseDate": "9 December 2024", + "version": 2024100701, + "releaseNoteUrl": false } ] }, @@ -49,6 +72,12 @@ "name": "4.4.4", "releaseDate": "7 October 2024", "version": 2024042204 + }, + { + "name": "4.4.5", + "releaseDate": "9 December 2024", + "version": 2024042205, + "releaseNoteUrl": false } ] }, @@ -106,6 +135,12 @@ "name": "4.3.8", "releaseDate": "7 October 2024", "version": 2023100908 + }, + { + "name": "4.3.9", + "releaseDate": "9 December 2024", + "version": 2023100909, + "releaseNoteUrl": false } ] }, @@ -265,6 +300,12 @@ "name": "4.1.14", "releaseDate": "7 October 2024", "version": 2022112814 + }, + { + "name": "4.1.15", + "releaseDate": "9 December 2024", + "version": 2022112815, + "releaseNoteUrl": false } ] }, @@ -2228,4 +2269,4 @@ "isLTS": false } ] -} \ No newline at end of file +}