Add a script to automate nested changelogs

Change-type: patch
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
This commit is contained in:
Thodoris Greasidis 2020-02-21 14:44:43 +02:00
parent 6439aa5552
commit f2be811e18
4 changed files with 166 additions and 1 deletions

View File

@ -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

140
automation/update-module.ts Normal file
View File

@ -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<string> => {
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();

View File

@ -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": [

View File

@ -1,3 +1,6 @@
type: node-cli
release: github
publishMetadata: true
publishMetadata: true
upstream:
- repo: 'balena-sdk'
url: 'https://github.com/balena-io/balena-sdk'