Last active
April 7, 2020 11:19
-
-
Save johnbeech/716d76bc16145976117f7f9d877fb821 to your computer and use it in GitHub Desktop.
Multi-repo software dependency updater - for projects using node, npm, and github
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
## Software Dependency Updater | |
A self contained multi-repo software dependency updater script - for projects using node, npm, and github. | |
## Prerequisites | |
- node > 10 | |
- npm > 6 | |
- hub > 2.14 with a github personal access token set on your env, e.g. GITHUB_TOKEN="xyz" | |
## Usage | |
1. Save this file to a temp directory, e.g. ~/workspace/software-updater/run.js | |
2. Modify the const repos = [] array to include all the node/package.json repos you want to target | |
3. Run `node run.js` | |
4. Hub should open PRs in your default web browser - check the diffs and merge as aproprirately | |
5. Check the logs/ file for per-repo reports | |
*/ | |
/* Embedded promisified exec */ | |
const execSync = require('child_process').exec | |
const exec = (command, options) => { | |
return new Promise(function(resolve, reject) { | |
execSync(command, options, (error, stdout, stderr) => { | |
if (error) { | |
reject({ | |
stdout: (stdout || '').trim(), | |
stderr: (stderr || '').trim(), | |
error | |
}) | |
} else { | |
resolve({ | |
stdout: (stdout || '').trim(), | |
stderr: (stderr || '').trim() | |
}) | |
} | |
}) | |
}) | |
} | |
/* Embedded promisified fs operations */ | |
const { read, write, mkdir, stat, rename, safeCreateDirectory } = (() => { | |
const fs = require('fs') | |
const { promisify } = require('util') | |
const read = promisify(fs.readFile) | |
const write = promisify(fs.writeFile) | |
const mkdir = promisify(fs.mkdir) | |
const stat = promisify(fs.stat) | |
const rename = promisify(fs.rename) | |
const report = (...messages) => console.log('[Async FS]', ...messages) | |
const exists = async (itemPath) => { | |
try { | |
await stat(itemPath) | |
return true | |
} catch (err) { | |
return false | |
} | |
} | |
const safeCreateDirectory = async (directoryPath) => { | |
try { | |
await mkdir(directoryPath, { recursive: true }) | |
} catch (err) { | |
if (err && err.code === 'EEXIST') { | |
report(`Directory: ${directoryPath} already exists`) | |
} else { | |
report(err) | |
} | |
} | |
} | |
const readJson = async (jsonFilePath) => { | |
const jsonFile = await read(jsonFilePath, 'utf-8') | |
return JSON.parse(jsonFile) | |
} | |
return { | |
read, | |
write, | |
mkdir, | |
exists, | |
readJson, | |
rename, | |
safeCreateDirectory | |
} | |
})() | |
/* Start of software updater script */ | |
console.log('Software Dependency Updater') | |
const repos = [ | |
'connected-web/boardgames-browser', | |
'connected-web/diplo', | |
'connected-web/gamebot', | |
'connected-web/boardgames-sam-api', | |
'connected-web/cards-against-x', | |
'connected-web/paws', | |
'connected-web/html-asset-generator', | |
'connected-web/product-monitor', | |
'mkv25-games/gamebase', | |
'connected-web/product-monitor.plugin.local-auth', | |
'connected-web/promise-path', | |
'connected-web/tasklist', | |
'connected-web/time-until', | |
'connected-web/medication-tracker', | |
'connected-web/quick-score', | |
'connected-web/town-builder-game', | |
'connected-web/guessdate-en', | |
'connected-web/remote-model' | |
] | |
console.log('Running against:') | |
console.log(repos.map((repo, i) => `\t[${i}] ${repo}`).join('\n')) | |
console.log('') | |
const startwd = process.cwd() | |
function reportResults(results) { | |
results.forEach((result, i) => { | |
const error = result.error || {} | |
const stringError = (typeof error === 'string') ? error : false | |
const formats = [`[${i}] ${result.repo}`, error.message, error.stdout, error.stderr, stringError, 'Completed OK'].filter(n => n) | |
console.log(...formats) | |
}) | |
} | |
async function updateRepo(repo, i) { | |
console.log(`[${i}]`, 'Starting some work:', repo) | |
const ticketId = 'UPDATER' | |
const branchName = 'software-update' | |
const commitMessage = 'Automatated software update\n\nUpdate all package and package-lock dependencies' | |
const seeMoreLink = '' | |
const logs = [] | |
function report(...messages) { | |
const now = new Date() | |
const format = [`[${now}] [${repo}]`].concat(messages).filter(n => n) | |
logs.push(format.join(' ')) | |
console.log(...format) | |
} | |
function quietReport(...messages) { | |
const now = new Date() | |
const format = [`[${now}] [${repo}]`].concat(messages).filter(n => n) | |
logs.push(format.join(' ')) | |
} | |
const cwd = `${startwd}/repos/${repo}` | |
let commandSteps = [ | |
{ command: `npm -v`, cwd: startwd }, | |
{ command: `git clone git@github.com:${repo}.git repos/${repo}`, cwd: startwd }, | |
{ command: `pwd`, cwd }, | |
{ command: `git push --delete origin ${branchName}`, cwd, optionalSuccess: true }, | |
{ command: `git checkout -b ${branchName}`, cwd }, | |
{ command: `npm outdated -l`, cwd, optionalSuccess: true }, | |
{ command: `rm package-lock.json`, cwd }, | |
{ command: `rm -rf node_modules`, cwd }, | |
{ command: `npm install`, cwd }, | |
{ command: `npm update`, cwd }, | |
{ command: `npm audit fix`, cwd }, | |
{ command: `npm outdated -l`, cwd, optionalSuccess: true }, | |
{ command: `git add package.json`, cwd }, | |
{ command: `git add package-lock.json`, cwd }, | |
{ command: `git commit -m "${ticketId} ${commitMessage}"`, cwd }, | |
{ command: `git status`, cwd }, | |
{ command: `git push --set-upstream origin ${branchName}`, cwd }, | |
{ command: `hub pull-request -m "${ticketId} ${commitMessage}\n\n${seeMoreLink}"`, cwd}, | |
{ command: `git status`, cwd }, | |
{ command: `hub pr show`, cwd } | |
] | |
async function executeCommand (item) { | |
let result = false | |
if (typeof item.command === 'string') { | |
report(item.command, 'from', item.cwd) | |
result = await exec(item.command, item) | |
report(result.stdout || result.stderr) | |
} | |
else if (typeof item.command === 'function') { | |
result = await item.command(item) | |
report(result) | |
} | |
else { | |
throw new Error('Unknown command type', item, 'for', repo) | |
} | |
return result | |
} | |
let error = false | |
let result = false | |
try { | |
// run each command in sequence | |
while (commandSteps.length > 0) { | |
let nextCommand = commandSteps.shift() | |
if (nextCommand.optionalSuccess) { | |
try { | |
result = await executeCommand(nextCommand) | |
} | |
catch (ex) { | |
quietReport('Optional command failed, continuing:', ex.stderr, ex.stdout, ex.error, ex.message) | |
} | |
} else { | |
result = await executeCommand(nextCommand) | |
} | |
} | |
} catch (ex) { | |
report('Caught exception:', ex.stderr, ex.stdout, ex.error, ex.message) | |
error = ex | |
} | |
const logFile = `${repo.replace(/\//g, '-')}.log` | |
report('Writing log file to', logFile) | |
await write(`logs/${logFile}`, logs.join('\n')) | |
// Return the results | |
return { | |
repo, | |
error | |
} | |
} | |
async function run() { | |
await exec(`rm -rf repos`) | |
await exec(`mkdir -p repos`) | |
await exec(`rm -rf logs`) | |
await exec(`mkdir -p logs`) | |
const pendingWork = repos.map(updateRepo) | |
results = await Promise.all(pendingWork) | |
reportResults(results) | |
} | |
run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment