From 45ee2cfa6ba63104d79e1cad4e479d67d3d5a9b5 Mon Sep 17 00:00:00 2001 From: andersonbosa Date: Sun, 12 May 2024 13:32:04 -0300 Subject: [PATCH] feat: add changelog generation script This commit introduces a changelog generation script written in JavaScript. The script leverages the Google Gemini API to automatically generate release overview summaries. Additionally, it ensures that only semantic commit messages are included in the changelog. --- moshell.sh/tools/changelog.js | 145 ++++++++++++++++++++++++++++++++++ moshell.sh/tools/changelog.sh | 6 +- 2 files changed, 148 insertions(+), 3 deletions(-) create mode 100755 moshell.sh/tools/changelog.js diff --git a/moshell.sh/tools/changelog.js b/moshell.sh/tools/changelog.js new file mode 100755 index 0000000..3bef5d7 --- /dev/null +++ b/moshell.sh/tools/changelog.js @@ -0,0 +1,145 @@ +#!/usr/bin/env node + +const { execSync } = require('node:child_process') +const { dirname, join, resolve } = require('node:path') +const fs = require('node:fs') +const os = require('node:os') +const https = require('node:https') + +// Defines the absolute path of the script file and the script directory +const ABSOLUTE_SCRIPT_FILE_PATH = process.argv[1] +const ABSOLUTE_SCRIPT_DIR_PATH = dirname(ABSOLUTE_SCRIPT_FILE_PATH) +const { GOOGLE_GEMINI_API_KEY } = process.env + +function getLastNthReleaseCommit (nth) { + if (!nth) throw new Error('nth is required') + const RELEASE_VERSION_GREP_REGEX = 'release([0-9]\\+\\.[0-9]\\+\\.[0-9]\\+)' + const awkQueryFirstItemOnTheRow = `NR==${nth}` + const gitLogCommand = `git log --pretty=format:%H --grep="${RELEASE_VERSION_GREP_REGEX}" | awk ${awkQueryFirstItemOnTheRow}` + const lastReleaseCommit = execSync(gitLogCommand).toString().trim() + return lastReleaseCommit +} + +// Adds header levels based on the Commit key +function parseCommitFormat (commitHash, _author, date, message) { + const linkToRepository = "https://github.com/andersonbosa/moshell.sh" + const isRelease = message.startsWith("release") + if (isRelease) { + return `## ${date} - ${message} ${os.EOL}${os.EOL}> - [Commit ${commitHash}](${linkToRepository}/commit/${commitHash})${os.EOL}TBD` + } else { + return `- ${message}` + } +} + +function isSemanticCommit (commitMessage) { + const semanticCommitKeys = ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'perf', 'build', 'ci', 'revert', 'release'] + const verifyCommit = key => commitMessage?.toLowerCase().startsWith(key.toLowerCase()) + return semanticCommitKeys.some(verifyCommit) +} + +// Analyzes an input line and converts it into a changelog format +function parseLineToChangelog (acc, line) { + const [commitHash, author, date, message] = line.split(",") + + if (!isSemanticCommit(message)) return acc + + const changelogEntry = parseCommitFormat(commitHash, author, date, message) + + return [...acc, changelogEntry] +} + + +async function generateReleaseOverviewWithGoogleGemini (releaseContent, aditionalInfo = '') { + if (!GOOGLE_GEMINI_API_KEY) { + return '' + } + + const prompt = ` +Your mission is to write paragraph about this release using the context from the commits provided below. Write a brief resume about what happened. Don't add header to text neither don't use lists. Do not quote the current version, just focus on what was done. Do not talk about the version being updated. + +\`\`\` +${releaseContent} +\`\`\` + +${aditionalInfo}` + + const options = { + hostname: 'generativelanguage.googleapis.com', + path: '/v1beta/models/gemini-pro:generateContent?key=' + GOOGLE_GEMINI_API_KEY, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + } + + const response = await new Promise((resolve, reject) => { + const req = https.request(options, (response) => { + let data = '' + response.on('data', (chunk) => { + data += chunk + }) + response.on('end', () => { + try { + const responseData = JSON.parse(data) + const contentOutput = responseData.candidates[0].content.parts[0].text + resolve(contentOutput) + } catch (error) { + reject(new Error('Error parsing response: ' + error.message)) + } + }) + }) + + req.on('error', (e) => { + reject(new Error('Error generating content: ' + e.message)) + }) + + const requestBody = JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }) + + req.write(requestBody) + req.end() + }) + + return response +} + +function getCommitsFromCurrentVersion (fromCommit, sinceCommit) { + if (!fromCommit || !sinceCommit) throw new Error('fromCommit and sinceCommit are required') + + const gitLogCommand = `git log --pretty=format:%h,%an,%as,%s "${fromCommit}...${sinceCommit}"` + const commitsFromTheCurrentVersion = execSync(gitLogCommand).toString().split(os.EOL) + return commitsFromTheCurrentVersion +} + +// Main function to generate Changelog +async function generateChangelog (outputPath = 'CHANGELOG.md') { + const changelogFilePath = join(`${ABSOLUTE_SCRIPT_DIR_PATH}`, '../..', '/docs/CHANGELOG.md') + + let changelogFileContentBackup = "" + if (fs.existsSync(changelogFilePath)) { + changelogFileContentBackup = fs.readFileSync(changelogFilePath, 'utf-8') + } + + // Limpa/inicializa o arquivo + // fs.writeFileSync(changelogFilePath, '') + + const fromCommit = getLastNthReleaseCommit(1) + const sinceCommit = getLastNthReleaseCommit(2) + const commitsFromTheCurrentVersion = getCommitsFromCurrentVersion(fromCommit, sinceCommit) + + const changelogLines = commitsFromTheCurrentVersion.reduce(parseLineToChangelog, []) + + const changelogContentFromNewVersion = changelogLines.join(os.EOL) + const geminiResponse = await generateReleaseOverviewWithGoogleGemini(changelogContentFromNewVersion) + + const changelogContent = changelogContentFromNewVersion + .replace('TBD', `${os.EOL}${geminiResponse}${os.EOL}`) + .concat(os.EOL) + .concat(os.EOL) + .concat(`${changelogFileContentBackup}`) + + fs.writeFileSync(outputPath, changelogContent) + // fs.appendFileSync(changelogFilePath, `\n${changelogFileContentBackup}`) +} + +const outputPath = process.argv[2] +generateChangelog(outputPath) diff --git a/moshell.sh/tools/changelog.sh b/moshell.sh/tools/changelog.sh index 492ac99..b3129c8 100755 --- a/moshell.sh/tools/changelog.sh +++ b/moshell.sh/tools/changelog.sh @@ -18,8 +18,8 @@ function __moshell:tools::changelog::get_last_release_commit() { release_version_regex=$default_regex fi - local awk_query_2_item_on_the_row='NR==2' - local last_release_commit=$(git log --grep="$release_version_regex" --pretty=format:"%H" | awk "$awk_query_2_item_on_the_row") + local awk_query_first_item_on_the_row='NR==1' + local last_release_commit=$(git log --grep=$release_version_regex --pretty=format:"%H" | awk "$awk_query_first_item_on_the_row") echo "$last_release_commit" } @@ -75,7 +75,7 @@ function __moshell:tools::changelog::main() { fi # Cleans/init the file - >"$changelog_file" + echo '' >"$changelog_file" local tmp_file=$(mktemp) git log --pretty=format:"%h,%an,%as,%s" "$LAST_RELEASE_COMMIT..HEAD" >"$tmp_file"