diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2156a7f0..44a732de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,6 +81,27 @@ reformats the code (based on configuration in the `node_modules/resin-lint/confi file). Beyond that, we have a preference for Javascript promises over callbacks, and for `async/await` over `.then()`. +## Updating upstream dependencies + +In order to get proper nested changelogs, when updating upstream modules that are in the repo.yml +(like the balena-sdk), the commit body has to contain a line with the following format: +``` +Update balena-sdk from 12.0.0 to 12.1.0 +``` + +Since this is error prone, it's suggested to use the following npm script: +``` +npm run update balena-sdk ^12.1.0 +``` +This will create a new branch (only if you are currently on master), run `npm update` with the +version you provided as a target and commit the package.json & npm-shrinkwrap.json. The script by +default will set the `Change-type` to `patch` or `minor`, depending on the semver change of the +updated dependency, but if you need to use a different one (eg `major`) you can specify it as an +extra argument: +``` +npm run update balena-sdk ^12.14.0 patch +npm run update balena-sdk ^13.0.0 major + ## Common gotchas One thing that most CLI bugs have in common is the absence of test cases exercising the broken diff --git a/automation/update-module.ts b/automation/update-module.ts new file mode 100644 index 00000000..25a13d1e --- /dev/null +++ b/automation/update-module.ts @@ -0,0 +1,140 @@ +import { exec } from 'child_process'; +import * as semver from 'semver'; + +const changeTypes = ['major', 'minor', 'patch'] as const; + +const validateChangeType = (maybeChangeType: string = 'minor') => { + maybeChangeType = maybeChangeType.toLowerCase(); + switch (maybeChangeType) { + case 'patch': + case 'minor': + case 'major': + return maybeChangeType; + default: + console.error(`Invalid change type: '${maybeChangeType}'`); + return process.exit(1); + } +}; + +const compareSemverChangeType = (oldVersion: string, newVersion: string) => { + const oldSemver = semver.parse(oldVersion)!; + const newSemver = semver.parse(newVersion)!; + + for (const changeType of changeTypes) { + if (oldSemver[changeType] !== newSemver[changeType]) { + return changeType; + } + } +}; + +const run = async (cmd: string) => { + console.info(`Running '${cmd}'`); + return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { + const p = exec(cmd, { encoding: 'utf8' }, (err, stdout, stderr) => { + if (err) { + reject(err); + return; + } + resolve({ stdout, stderr }); + }); + p.stdout.pipe(process.stdout); + p.stderr.pipe(process.stderr); + }); +}; + +const getVersion = async (module: string): Promise => { + const { stdout } = await run(`npm ls --json --depth 0 ${module}`); + return JSON.parse(stdout).dependencies[module].version; +}; + +interface Upstream { + repo: string; + url: string; + module?: string; +} + +const getUpstreams = async () => { + const fs = await import('fs'); + const repoYaml = fs.readFileSync(__dirname + '/../repo.yml', 'utf8'); + + const yaml = await import('js-yaml'); + const { upstream } = yaml.safeLoad(repoYaml) as { + upstream: Upstream[]; + }; + + return upstream; +}; + +const printUsage = (upstreams: Upstream[], upstreamName: string) => { + console.error( + ` +Usage: npm run update ${upstreamName} $version [$changeType=minor] + +Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')} +`, + ); + return process.exit(1); +}; + +// TODO: Drop the wrapper function once we move to TS 3.8, +// which will support top level await. +async function main() { + const upstreams = await getUpstreams(); + + if (process.argv.length < 3) { + return printUsage(upstreams, '$upstreamName'); + } + + const upstreamName = process.argv[2]; + + const upstream = upstreams.find(v => v.repo === upstreamName); + + if (!upstream) { + console.error( + `Invalid upstream name '${upstreamName}', valid options: ${upstreams + .map(({ repo }) => repo) + .join(', ')}`, + ); + return process.exit(1); + } + + if (process.argv.length < 4) { + printUsage(upstreams, upstreamName); + } + + const packageName = upstream.module || upstream.repo; + + const oldVersion = await getVersion(packageName); + await run(`npm install ${packageName}@${process.argv[3]}`); + const newVersion = await getVersion(packageName); + if (newVersion === oldVersion) { + console.error(`Already on version '${newVersion}'`); + return process.exit(1); + } + + console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`); + const semverChangeType = compareSemverChangeType(oldVersion, newVersion); + + const changeType = process.argv[4] + ? // if the caller specified a change type, use that one + validateChangeType(process.argv[4]) + : // use the same change type as in the dependency, but avoid major bumps + semverChangeType && semverChangeType !== 'major' + ? semverChangeType + : 'minor'; + console.log(`Using Change-type: ${changeType}`); + + let { stdout: currentBranch } = await run('git rev-parse --abbrev-ref HEAD'); + currentBranch = currentBranch.trim(); + console.log(`Currenty on branch: '${currentBranch}'`); + if (currentBranch === 'master') { + await run(`git checkout -b "update-${upstreamName}-${newVersion}"`); + } + + await run(`git add package.json npm-shrinkwrap.json`); + await run( + `git commit --message "Update ${upstreamName} to ${newVersion}" --message "Update ${upstreamName} from ${oldVersion} to ${newVersion}" --message "Change-type: ${changeType}"`, + ); +} + +main(); diff --git a/package.json b/package.json index 39368882..adae2b88 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "watch": "gulp watch", "prettify": "prettier --write \"{lib,tests,automation,typings}/**/*.js\" --config ./node_modules/resin-lint/config/.prettierrc", "lint": "resin-lint lib/ tests/ && resin-lint --typescript --fix automation/ lib/ typings/ tests/", + "update": "ts-node --transpile-only -P automation/tsconfig.json ./automation/update-module.ts", "prepublishOnly": "npm run build" }, "keywords": [ diff --git a/repo.yml b/repo.yml index f9b27391..6a628e11 100644 --- a/repo.yml +++ b/repo.yml @@ -1,3 +1,6 @@ type: node-cli release: github -publishMetadata: true \ No newline at end of file +publishMetadata: true +upstream: + - repo: 'balena-sdk' + url: 'https://github.com/balena-io/balena-sdk'