|
| 1 | +/* |
| 2 | + * Copyright OpenSearch Contributors |
| 3 | + * SPDX-License-Identifier: Apache-2.0 |
| 4 | + */ |
| 5 | + |
| 6 | +import { join, resolve } from 'path'; |
| 7 | +import { readFileSync, writeFileSync, Dirent, rm, rename, promises as fsPromises } from 'fs'; |
| 8 | +import { load as loadYaml } from 'js-yaml'; |
| 9 | +import { readdir } from 'fs/promises'; |
| 10 | +import { version as pkgVersion } from '../../package.json'; |
| 11 | +import { |
| 12 | + validateFragment, |
| 13 | + getCurrentDateFormatted, |
| 14 | + Changelog, |
| 15 | + SECTION_MAPPING, |
| 16 | + fragmentDirPath, |
| 17 | + SectionKey, |
| 18 | + releaseNotesDirPath, |
| 19 | + filePath, |
| 20 | +} from './generate_release_note_helper'; |
| 21 | + |
| 22 | +// Function to add content after the 'Unreleased' section in the changelog |
| 23 | +function addContentAfterUnreleased(path: string, newContent: string): void { |
| 24 | + let fileContent = readFileSync(path, 'utf8'); |
| 25 | + const targetString = '## [Unreleased]'; |
| 26 | + const targetIndex = fileContent.indexOf(targetString); |
| 27 | + |
| 28 | + if (targetIndex !== -1) { |
| 29 | + const endOfLineIndex = fileContent.indexOf('\n', targetIndex); |
| 30 | + if (endOfLineIndex !== -1) { |
| 31 | + fileContent = |
| 32 | + fileContent.slice(0, endOfLineIndex + 1) + |
| 33 | + '\n' + |
| 34 | + newContent + |
| 35 | + '\n' + |
| 36 | + fileContent.slice(endOfLineIndex + 1); |
| 37 | + } else { |
| 38 | + throw new Error('End of line for "Unreleased" section not found.'); |
| 39 | + } |
| 40 | + } else { |
| 41 | + throw new Error("'## [Unreleased]' not found in the file."); |
| 42 | + } |
| 43 | + |
| 44 | + writeFileSync(path, fileContent); |
| 45 | +} |
| 46 | + |
| 47 | +async function deleteFragments(fragmentTempDirPath: string) { |
| 48 | + rm(fragmentTempDirPath, { recursive: true }, (err: any) => { |
| 49 | + if (err) { |
| 50 | + throw err; |
| 51 | + } |
| 52 | + }); |
| 53 | +} |
| 54 | + |
| 55 | +// Read fragment files and populate sections |
| 56 | +async function readFragments() { |
| 57 | + // Initialize sections |
| 58 | + const sections: Changelog = (Object.fromEntries( |
| 59 | + Object.keys(SECTION_MAPPING).map((key) => [key, []]) |
| 60 | + ) as unknown) as Changelog; |
| 61 | + |
| 62 | + const fragmentPaths = await readdir(fragmentDirPath, { withFileTypes: true }); |
| 63 | + for (const fragmentFilename of fragmentPaths) { |
| 64 | + // skip non yml or yaml files |
| 65 | + if (!/\.ya?ml$/i.test(fragmentFilename.name)) { |
| 66 | + // eslint-disable-next-line no-console |
| 67 | + console.warn(`Skipping non yml or yaml file ${fragmentFilename.name}`); |
| 68 | + continue; |
| 69 | + } |
| 70 | + |
| 71 | + const fragmentPath = join(fragmentDirPath, fragmentFilename.name); |
| 72 | + const fragmentContents = readFileSync(fragmentPath, { encoding: 'utf-8' }); |
| 73 | + |
| 74 | + validateFragment(fragmentContents); |
| 75 | + |
| 76 | + const fragmentYaml = loadYaml(fragmentContents) as Changelog; |
| 77 | + |
| 78 | + for (const [sectionKey, entries] of Object.entries(fragmentYaml)) { |
| 79 | + sections[sectionKey as SectionKey].push(...entries); |
| 80 | + } |
| 81 | + } |
| 82 | + return { sections, fragmentPaths }; |
| 83 | +} |
| 84 | + |
| 85 | +async function moveFragments(fragmentPaths: Dirent[], fragmentTempDirPath: string): Promise<void> { |
| 86 | + // Move fragment files to temp fragments folder |
| 87 | + for (const fragmentFilename of fragmentPaths) { |
| 88 | + const fragmentPath = resolve(fragmentDirPath, fragmentFilename.name); |
| 89 | + const fragmentTempPath = resolve(fragmentTempDirPath, fragmentFilename.name); |
| 90 | + rename(fragmentPath, fragmentTempPath, () => {}); |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +function generateChangelog(sections: Changelog) { |
| 95 | + // Generate changelog sections |
| 96 | + const changelogSections = Object.entries(sections).map(([sectionKey, entries]) => { |
| 97 | + const sectionName = SECTION_MAPPING[sectionKey as SectionKey]; |
| 98 | + return entries.length === 0 |
| 99 | + ? `### ${sectionName}` |
| 100 | + : `### ${sectionName}\n\n${entries.map((entry) => ` - ${entry}`).join('\n')}`; |
| 101 | + }); |
| 102 | + |
| 103 | + // Generate full changelog |
| 104 | + const currentDate = getCurrentDateFormatted(); |
| 105 | + const changelog = `## [${pkgVersion}-${currentDate}](https://github.com/opensearch-project/OpenSearch-Dashboards/releases/tag/${pkgVersion})\n\n${changelogSections.join( |
| 106 | + '\n\n' |
| 107 | + )}`; |
| 108 | + // Update changelog file |
| 109 | + addContentAfterUnreleased(filePath, changelog); |
| 110 | + return changelogSections; |
| 111 | +} |
| 112 | + |
| 113 | +function generateReleaseNote(changelogSections: string[]) { |
| 114 | + // Generate release note |
| 115 | + const releaseNoteFilename = `opensearch-dashboards.release-notes-${pkgVersion}.md`; |
| 116 | + const releaseNoteHeader = `# VERSION ${pkgVersion} Release Note`; |
| 117 | + const releaseNote = `${releaseNoteHeader}\n\n${changelogSections.join('\n\n')}`; |
| 118 | + writeFileSync(resolve(releaseNotesDirPath, releaseNoteFilename), releaseNote); |
| 119 | +} |
| 120 | + |
| 121 | +(async () => { |
| 122 | + const { sections, fragmentPaths } = await readFragments(); |
| 123 | + // create folder for temp fragments |
| 124 | + const fragmentTempDirPath = await fsPromises.mkdtemp(join(fragmentDirPath, 'tmp_fragments-')); |
| 125 | + // move fragments to temp fragments folder |
| 126 | + await moveFragments(fragmentPaths, fragmentTempDirPath); |
| 127 | + |
| 128 | + const changelogSections = generateChangelog(sections); |
| 129 | + |
| 130 | + generateReleaseNote(changelogSections); |
| 131 | + |
| 132 | + // remove temp fragments folder |
| 133 | + await deleteFragments(fragmentTempDirPath); |
| 134 | +})(); |
0 commit comments