From 5711eb065d72259b335bf840e7905cb35f952db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Sat, 18 Jan 2020 11:14:23 +0100 Subject: [PATCH] [ADD] tooling: add release script More work is needed to complete this, but the basics is here, and it is difficult to test. closes #612 --- .gitignore | 4 +- package.json | 10 ++- tools/release.js | 215 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 tools/release.js diff --git a/.gitignore b/.gitignore index 3f634e56..52ddd556 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ package-lock.json node_modules # Extras temp file -/tools/owl.js \ No newline at end of file +/tools/owl.js + +release-notes.md \ No newline at end of file diff --git a/package.json b/package.json index 52754d27..70de4697 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,10 @@ "description": "Odoo Web Library (OWL)", "main": "dist/owl.js", "types": "dist/types/index.d.ts", - "files": ["dist/types/", "dist/owl.js"], + "files": [ + "dist/types/", + "dist/owl.js" + ], "engines": { "node": ">=10.15.3" }, @@ -21,7 +24,8 @@ "pretools:watch": "npm run build", "tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"", "prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write", - "publish": "npm run buildcommonjs && npm publish" + "publish": "npm run buildcommonjs && npm publish", + "release": "node tools/release.js" }, "repository": { "type": "git", @@ -36,8 +40,10 @@ "dependencies": {}, "devDependencies": { "@types/jest": "^23.3.12", + "chalk": "^3.0.0", "cpx": "^1.5.0", "git-rev-sync": "^1.12.0", + "github-api": "^3.3.0", "jest": "^23.6.0", "jest-environment-jsdom": "^24.7.1", "live-server": "^1.2.1", diff --git a/tools/release.js b/tools/release.js new file mode 100644 index 00000000..e98f4bb1 --- /dev/null +++ b/tools/release.js @@ -0,0 +1,215 @@ +const package = require("../package.json"); +const readline = require("readline"); +const fs = require("fs"); +const exec = require("child_process").exec; +const chalk = require("chalk"); +const GitHub = require("github-api"); + +const REL_NOTES_FILE = `release-notes.md`; +const STEPS = 10; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +startRelease().then(() => { + rl.close(); +}); + +// ----------------------------------------------------------------------------- +// Relase Script +// ----------------------------------------------------------------------------- + +async function startRelease() { + log(`*** Owl release script ***`); + log(`Current Version: ${package.version}`); + + // --------------------------------------------------------------------------- + log(`Step 1/${STEPS}: collecting info...`); + const current = package.version; + const next = await ask("Next version: "); + let file = await ask(`Release notes (${REL_NOTES_FILE}): `); + file = file || REL_NOTES_FILE; + let content; + try { + content = await readFile("./" + file); + } catch (e) { + logSubContent(e.message); + log("Cannot find release notes... Aborting"); + return; + } + + // Todo: add playground update feature + // let shouldUpdateStr = await ask("Update Playground? (y/n)"); + // const shouldUpdatePlayground = shouldUpdateStr === "y"; + const token = await ask("Github token: "); + + // --------------------------------------------------------------------------- + log(`Step 2/${STEPS}: running tests...`); + const testsResult = await execCommand("npm run test"); + if (testsResult !== 0) { + log("Test suite does not pass. Aborting."); + return; + } + ask("Ready for next step..."); + + // --------------------------------------------------------------------------- + log(`Step 3/${STEPS}: updating package.json, readme.md and roadmap.md...`); + await replaceInFile("./package.json", current, next); + await replaceInFile("./README.md", current, next); + await replaceInFile("./roadmap.md", current, next); + ask("Ready for next step..."); + + // --------------------------------------------------------------------------- + log(`Step 4/${STEPS}: creating git commit...`); + const gitResult = await execCommand(`git commit -am "[REL] v${next}\n\n${content}"`); + if (gitResult !== 0) { + log("Git commit failed. Aborting."); + return; + } + ask("Ready for next step..."); + + // -------------------j-------------------------------------------------------- + log(`Step 5/${STEPS}: building owl (iife version)...`); + const buildResult = await execCommand("npm run build"); + if (buildResult !== 0) { + log("Build failed. Aborting."); + return; + } + ask("Ready for next step..."); + + // --------------------------------------------------------------------------- + log(`Step 6/${STEPS}: minifying owl...`); + const minifyResult = await execCommand("npm run minify"); + if (minifyResult !== 0) { + log("Minify failed. Aborting."); + return; + } + ask("Ready for next step..."); + + // --------------------------------------------------------------------------- + log(`Step 7/${STEPS}: pushing on github...`); + const pushResult = await execCommand("git push"); + if (pushResult !== 0) { + log("git push failed. Aborting."); + return; + } + ask("Ready for next step..."); + + // --------------------------------------------------------------------------- + log(`Step 8/${STEPS}: publishing release notes on github...`); + const options = { + tag_name: `v${next}`, + name: `v${next}`, + body: content, + draft: true // todo: remove this someday + }; + const result = await createRelease(token, options); + console.log(result); + ask("Ready for next step..."); + + // --------------------------------------------------------------------------- + log(`Step 9/${STEPS}: adding assets to release...`); + await ask("Please add owl.js and owl.min.js to draft release, then confirm"); + // todo: do this with curl + // curl \ + // -H "Authorization: token $GITHUB_TOKEN" \ + // -H "Content-Type: $(file -b --mime-type $FILE)" \ + // --data-binary @$FILE \ + // "https://uploads.github.com/repos/hubot/singularity/releases/123/assets?name=$(basename $FILE)" + + // --------------------------------------------------------------------------- + log(`Step 10/${STEPS}: publishing module on npm...`); + await execCommand("npm run publish"); + + log("Owl Release process completed! Thank you for your patience"); +} + +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +function log(text) { + console.log(chalk.yellow(formatLog(text))); +} + +function formatLog(text) { + return `[REL] ${text}`; +} + +function logSubContent(text) { + for (let line of text.split("\n")) { + if (line.trim()) { + console.log(" " + line); + } + } +} + +function ask(question) { + return new Promise(resolve => { + rl.question(question, result => { + resolve(result); + }); + }); +} + +function logStream(stream) { + stream.on("data", data => { + logSubContent(data); + }); +} + +function execCommand(command) { + return new Promise(resolve => { + const childProcess = exec(command, (err, stdout, stderr) => { + if (err) { + resolve(err.code); + } + }); + childProcess.on("exit", code => { + resolve(code); + }); + logStream(childProcess.stdout); + logStream(childProcess.stderr); + }); +} + +function readFile(file) { + return new Promise((resolve, reject) => { + fs.readFile(file, "utf8", function(err, content) { + if (err) { + reject(err); + } else { + resolve(content); + } + }); + }); +} + +async function replaceInFile(file, from, to) { + const content = await readFile(file); + return new Promise((resolve, reject) => { + const updatedContent = content.replace(new RegExp(from, "g"), to); + fs.writeFile(file, updatedContent, "utf8", err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +function createRelease(token, options) { + return new Promise((resolve, reject) => { + var gh = new GitHub({ token }); + gh.getRepo("odoo", "owl").createRelease(options, (err, result, req) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + }); +}