diff --git a/.gitignore b/.gitignore index baf1b426..b0061c12 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ Gemfile.lock # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc + +# Dependency directory +node_modules diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..dba9347b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,108 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "bl": { + "version": "1.0.3", + "resolved": "http://registry.npmjs.org/bl/-/bl-1.0.3.tgz", + "integrity": "sha1-/FQhoo/UImA2w7OJGmaiW8ZNIm4=", + "dev": true, + "requires": { + "readable-stream": "~2.0.5" + } + }, + "changelog-parser": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/changelog-parser/-/changelog-parser-2.5.0.tgz", + "integrity": "sha1-XWeqJGPPUI7xQV8AZmlZXH/h3qM=", + "dev": true, + "requires": { + "line-reader": "^0.2.4", + "remove-markdown": "^0.2.2" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "gitexec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gitexec/-/gitexec-1.0.0.tgz", + "integrity": "sha1-rFicoxd6mUJ0Zao3sfgXF2weNCI=", + "dev": true, + "requires": { + "bl": "~1.0.0" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "line-reader": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/line-reader/-/line-reader-0.2.4.tgz", + "integrity": "sha1-xDkrWH3qOFgMlnhXDm6OSfzlJiI=", + "dev": true + }, + "node-fetch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.2.0.tgz", + "integrity": "sha512-OayFWziIxiHY8bCUyLX6sTpDH8Jsbp4FfYd1j1f7vZyfgkcOnAyM4oQR16f8a0s7Gl/viMGRey8eScYk4V4EZA==", + "dev": true + }, + "parse-github-url": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz", + "integrity": "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "remove-markdown": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.2.2.tgz", + "integrity": "sha1-ZrDO66n7d8qWNrsbAwfOIaMqEqY=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..dd5bcdd8 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "author": "SendGrid (SendGrid.com)", + "contributors": [ + "Elmer Thomas " + ], + "devDependencies": { + "changelog-parser": "^2.5.0", + "gitexec": "^1.0.0", + "node-fetch": "^2.2.0", + "parse-github-url": "^1.0.2" + }, + "license": "MIT" +} diff --git a/scripts/update-changelog b/scripts/update-changelog new file mode 100755 index 00000000..44c687a5 --- /dev/null +++ b/scripts/update-changelog @@ -0,0 +1,267 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const git = require('gitexec'); +const path = require('path'); +const fetch = require('node-fetch'); +const child = require('child_process'); +const parser = require('changelog-parser'); +const githubURL = require('parse-github-url'); + +const ROOT_URL = 'https://api.github.com/graphql'; +const GITHUB_URL = 'https://github.com'; +const ROOT_PATH = path.dirname(__dirname); +const MAKER_PATH = path.join(ROOT_PATH, 'node_modules', 'changelog-maker', 'changelog-maker.js'); +const CHANGELOG_PATH = path.join(ROOT_PATH, 'CHANGELOG.md'); + +const ISSUE_REGEX = /#[1-9]\d*\b/g; +const MERGE_WORDS = 'Merge pull request'; + +const MAX_NUM = 50; +const FIX_WORDS = ['fix', 'resolve']; +const CHANGED_WORDS = ['change']; + +/** + * Child Process helper */ +function getGit(cmd) +{ + + return new Promise((res, rej) => { + const data = []; + const ctx = git.execCollect(__dirname, cmd, (err, result) => { + if (err) return rej(err); + res(result); + }); + }); +} + +function getRemoteOrigin() +{ + return getGit('git remote get-url upstream 2> /dev/null || git remote get-url origin').then(res => githubURL(res)); +} + +function getLastTagRef() +{ + return getGit('git rev-list -1 --tags=*.*.*').then(res => res.trim().replace('\n', '')); +} + +function getMergesSinceRef(ref) +{ + return getGit(`git log ${ref}..master`).then((res) => { + const lines = res.split('\n'); + return lines.filter((res) => { + return res.indexOf(MERGE_WORDS) !== -1; + }).map(item => item.trim()); + }); +} + + +function fetchPR (owner, name, number) +{ + const query = ` + query { + repository (owner: "${owner}", name: "${name}") + { + pullRequest(number: ${number}) { + title + number + mergedAt + bodyText + url + additions + deletions + author { + login + url + ... on User { + name + } + } + } + } + } + `; + + return fetch(ROOT_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `bearer ${process.env.GITHUB_TOKEN}` + }, + body: JSON.stringify({ query }) + }).then((res) => res.json()); +} + +async function fetchPRs (owner, name, since) +{ + const entries = await getMergesSinceRef(since).then(res => { + return res.map(item => { + const issue_str = item.match(ISSUE_REGEX)[0]; + return issue_str.substr(1); + }); + }); + + const requests = []; + for (let i = 0; i < entries.length; i++) + requests.push(fetchPR (owner, name, entries[i])); + return Promise.all(requests); +} + +/** + * Changelog Helpers */ +function checkAgainstWords (title, words) +{ + title = title.toLowerCase(); + for (let i = 0; i < words.length; i++) + if (title.indexOf(words[0]) !== -1) return true; + return false; +} +function groupChangelogItems (items) +{ + const added = []; + const fixed = []; + const changed = []; + + for (let i = 0; i < items.length; i++) + { + const item = items[i]; + const title = item.title; + + if (checkAgainstWords(title, FIX_WORDS)) + { + fixed.push(item); + continue; + } + + if (checkAgainstWords(title, CHANGED_WORDS)) + { + changed.push(item); + continue; + } + + added.push(item); + } + + return { added, fixed, changed }; +} + + +function writeChangelogItems (stream, type, data) +{ + const { items, repo } = data; + if (items.length < 1) return; + stream.write(`### ${type}\n`); + for (let i = 0; i < items.length; i++) + writeChangelogLine(stream, repo, items[i]); + stream.write('\n'); +} + +function writeChangelogHead (stream, title, description) +{ + stream.write(`# ${title}\n`); + stream.write(`${description}\n\n`); +} + +function writeChangelogEntry (stream, content) +{ + const items = groupChangelogItems(content.items); + const repo = content.repo; + + /** + * Date formatting */ + const now = new Date(); + const year = now.getFullYear(); + + let month = now.getMonth() + 1; + month = month < 10 ? `0${month}` : month; + + let day = now.getDate(); + day = day < 10 ? `0${day}` : month; + + /** + * Write changelog */ + stream.write(`## [${content.version}] - ${year}-${month}-${day}\n`); + writeChangelogItems(stream, 'Added', { repo, items: items.added }); + writeChangelogItems(stream, 'Changed', { repo, items: items.changed }); + writeChangelogItems(stream, 'Fixed', { repo, items: items.fixed }); + stream.write(`\n`); +} + +function writeChangelogExistingEntry (stream, content) +{ + stream.write(`## ${content.title}\n`); + stream.write(`${content.body}\n\n\n`); +} + +function writeChangelogLine (stream, repo, data) +{ + const author = data.author; + const name = author.name || author.login; + + const title = data.title || ''; + const description = data.bodyText || ''; + const firstLine = description.split('\n')[0]; + + + let line = '- '; + line += `[PR #${data.number}](${data.url}): ${data.title}`; + + if (firstLine) + { + /** + * We'll analyze the first line to see if + * we find a reference to an issue/PR */ + const has_issue = ISSUE_REGEX.test(firstLine); + if (has_issue) + { + const linked = firstLine.replace(ISSUE_REGEX, (match) => { + const issue_num = match.substr(1); + return `[${match}](${GITHUB_URL}/${repo}/issues/${issue_num})`; + }); + line += `, ${linked.toLowerCase()}`; + } + } + + line += `. Thanks [${name}](${author.url}) for the PR!\n`; + stream.write(line); +} + +/** + * Entry point */ +const version = process.argv[2]; +if (!version) +{ + console.error("No version number given"); + process.exit(1); +} + +parser(CHANGELOG_PATH) + .then(async originalContent => { + const origin = await getRemoteOrigin(); + const tagRef = await getLastTagRef(); + const items = await fetchPRs(origin.owner, origin.name, tagRef).then(res => { + return res.filter(item => !item.errors) + .map(item => item.data.repository.pullRequest); + }); + + if (items.length < 1) throw new Error("Couldn't fetch data from GitHub!"); + + /** + * Write new CHANGELOG.md */ + const stream = fs.createWriteStream(path.join(ROOT_PATH, 'CHANGELOG.md')); + writeChangelogHead(stream, originalContent.title, originalContent.description); + writeChangelogEntry(stream, { repo: `${origin.owner}/${origin.name}`, version, items }) + + /** + * Write the already existing items */ + const existingVersions = originalContent.versions; + for (let i = 0; i < existingVersions.length; i++) + writeChangelogExistingEntry(stream, existingVersions[i]); + + stream.end(); + console.log(`Changelog updated, the new version is ${version}`); + }) + .catch((err) => { + console.error(err); + process.exit(1); + });