Skip to content

Instantly share code, notes, and snippets.

@johnbeech
Last active April 7, 2020 11:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save johnbeech/716d76bc16145976117f7f9d877fb821 to your computer and use it in GitHub Desktop.
Save johnbeech/716d76bc16145976117f7f9d877fb821 to your computer and use it in GitHub Desktop.
Multi-repo software dependency updater - for projects using node, npm, and github
/*
## 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