Automate Releases

If your project follows a semantic versioning, it may be a good idea to automatize the steps needed to do a release. The recipe below bumps the project version, commits the changes to git and creates a new GitHub release.

For publishing a GitHub release you’ll need to create a personal access token and add it to your project. However, we don’t want to commit it, so we’ll use dotenv to load it from a git-ignored .env file:


1. GH_TOKEN=ff34885...

Don’t forget to add .env to your .gitignore.

Next, install all the necessary dependencies for this recipe:


1. npm install --save-dev conventional-recommended-bump conventional-changelog-cli conventional-github-releaser dotenv execa

Based on your environment, setup and preferences, your release workflow might look something like this:


1. const gulp = require('gulp');
2. const conventionalRecommendedBump = require('conventional-recommended-bump');
3. const conventionalGithubReleaser = require('conventional-github-releaser');
4. const execa = require('execa');
5. const fs = require('fs');
6. const { promisify } = require('util');
7. const dotenv = require('dotenv');

9. // load environment variables
10. const result = dotenv.config();

12. if (result.error) {
13. throw result.error;
14. }

16. // Conventional Changelog preset
17. const preset = 'angular';
18. // print output of commands into the terminal
19. const stdio = 'inherit';

21. async function bumpVersion() {
22. // get recommended version bump based on commits
23. const { releaseType } = await promisify(conventionalRecommendedBump)({ preset });
24. // bump version without committing and tagging
25. await execa('npm', ['version', releaseType, '--no-git-tag-version'], {
26. stdio,
27. });
28. }

30. async function changelog() {
31. await execa(
32. 'npx',
33. [
34. 'conventional-changelog',
35. '--preset',
36. preset,
37. '--infile',
38. 'CHANGELOG.md',
39. '--same-file',
40. ],
41. { stdio }
42. );
43. }

45. async function commitTagPush() {
46. // even though we could get away with "require" in this case, we're taking the safe route
47. // because "require" caches the value, so if we happen to use "require" again somewhere else
48. // we wouldn't get the current value, but the value of the last time we called "require"
49. const { version } = JSON.parse(await promisify(fs.readFile)('package.json'));
50. const commitMsg = `chore: release ${version}`;
51. await execa('git', ['add', '.'], { stdio });
52. await execa('git', ['commit', '--message', commitMsg], { stdio });
53. await execa('git', ['tag', `v${version}`], { stdio });
54. await execa('git', ['push', '--follow-tags'], { stdio });
55. }

57. function githubRelease(done) {
58. conventionalGithubReleaser(
59. { type: 'oauth', token: process.env.GH_TOKEN },
60. { preset },
61. done
62. );
63. }

65. exports.release = gulp.series(
66. bumpVersion,
67. changelog,
68. commitTagPush,
69. githubRelease
70. );