Skip to content

A tool for automated publishing of semantically correct versions of your packages.

License

Notifications You must be signed in to change notification settings

webiny/webiny-semantic-release

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

webiny-semantic-release

Commitizen friendly Prettier license

Branch Build Coverage
master (latest release) Build Status Coverage Status
development (active development) Build Status Coverage Status

A tool for automated and reliable versioning inspired by semantic-release.

  • supports single package repositories
  • supports monorepo structure (Lerna or custom)
  • supports plugins and 100% customizable release/publish process
  • detailed preview of actual release (dry run) if you want to preview what is about to happen

Why not simply use semantic-release?

Kudos to the semantic-release team for starting this movement! We greatly support it and think it is a very important step towards a stable open-source ecosystem.

But the primary problem for us was - it does not support monorepos. We did try to wrap it with some custom logic to make it work in a monorepo environment but we very soon hit a wall. We gave up when we had to update versions of inter-dependent packages (when one of the packages in your monorepo depends on another package in te same monorepo). There were also other problems, but these were the deciding ones.

Still, we use 2 of their plugins to analyze commits and generate release notes by simply wrapping them with our own plugins (read further) and adding some bells and whistles.

How to use

We provide a set of plugins you can use to get started quickly. Plugins are listed in the recommended order:

Order Plugin Description
1. githubVerify verifies GH_TOKEN or GITHUB_TOKEN and repo permissions.
2. npmVerify verifies NPM_TOKEN.
3. analyzeCommits analyzes commit history and determines version type.
4. updatePackage updates package version and versions of dependencies.
5. releaseNotes generates release notes for GitHub release.
6. githubPublish publishes a new release to GitHub.
7. npmPublish publishes the package to npm.
// We are using `require` to avoid having to transpile the code
const wsr = require("webiny-semantic-release");

// NOTE: you are responsible for defining an array of packages!
// This makes this tool very flexible as it does not care about your project structure,
// only about the packages you pass to it.

// For single package repos the package will be loaded automatically

// For Lerna packages read the section below

// For custom project structures you just need to specify your packages using the following template:
const projectPackages = [
    {
        name: 'package-1',
        location: '/my/project/packages/package-1',
        package: {
            // Here goes the ENTIRE content of `package.json` file
            name: 'package-1',
            version: '0.0.0-semantically-released',
            dependencies: {},
            // ...
        }
    }
];

// Run release (returns a Promise)
wsr.release({
    ci: true,
    preview: false,
    branch: 'master',
    packages: projectPackages,
    plugins: [
        wsr.githubVerify(),
        wsr.npmVerify(),
        wsr.analyzeCommits(),
        wsr.releaseNotes(),
        wsr.updatePackage(),
        wsr.githubPublish(),
        wsr.npmPublish()
    ]
}).catch(err => {
    console.log(err);
    process.exit(1);
});

Lerna packages

We did not include lerna as a dependency to keep things simple. Loading packages yourself is simple enough, here is a working example:

import Repository from "lerna/lib/Repository";
import PackageUtilities from "lerna/lib/PackageUtilities";

const packages = PackageUtilities.getPackages(new Repository())
    .filter(pkg => !pkg.isPrivate()) // do not include private packages
    .map(pkg => {
        return {
            name: pkg.name,
            location: pkg.location,
            package: pkg.toJSON()
        };
    });

Plugin system

Our plugin system is very straightforward. It works almost the same way express middleware does. Under the hood we use a very simple tool to compose middleware functions using arbitrary params, a next callback and a finish callback. params are passed to each consecutive plugin and are mutable so all plugins share data and can modify data.

These Flow types will make everything much clearer:

declare type Package = {
    name: string,
    location: string,
    package: Object
};

declare type Params = {
    packages: Array<Package>,
    logger: { log: Function, error: Function },
    git: Object,
    config: {
        ci: Boolean,
        preview: Boolean,
        repositoryUrl: string,
        branch: string,
        tagFormat: string | (pkg: Package) => string
    }
};

declare type Plugin = (params: Params, next: Function, finish: Function) => void;

Creating a plugin

To create a plugin you need to defined a simple function of type Plugin (see the Flow types above).

Let's create a plugin which checks all the packages for the presence of README.md file:

import path from "path";
import fs from "fs";

// I always recommend to export a factory function which can optionally take a config object.
// That way you are safe for possible future upgrades or parameters.
export default () => {
    return ({packages, logger}, next, finish) => {
        let errors = [];
        packages.map(pkg => {
            if (!fs.existsSync(path.join(pkg.location, "README.md"))) {
                errors.push(pkg.name);
            }
        });

        if (errors.length) {
            logger.error(
                "Missing README.md file in the following packages:\n\t- %s",
                errors.join("\n- ")
            );
			return finish();
        }
        next();
    }
};

Adding custom plugins to the release process

If you want to create your own publishing process and maybe remove or add some of the plugins, you can do it like this:

// We will remove the `npm` plugins, and instead add 2 new plugins

const wsr = require("webiny-semantic-release");

wsr.release({
    preview: true,
    plugins: [
        wsr.githubVerify(),
        checkReadmeFile(), // Use your new plugin for README.md verification (see "Creating a plugin")
        wsr.analyzeCommits(),
        wsr.releaseNotes(),
        // This plugin will modify the release notes of each package and add a custom footer.
        // After the `releaseNotes` plugin did its job, each package `nextRelease` will contain a `notes` key with the generated notes.
        ({packages}, next) => {
            packages.map(pkg => {
                pkg.nextRelease && pkg.nextRelease.notes += "\nI MUST have this at the bottom of each release!"
            });
            next();
        },
        // Publish plugin will now use the new `notes` because they were modified by your plugin
        wsr.githubPublish()
    ]
}).catch(err => {
    console.error(err);
    process.exit(1);
});

Filtering relevant commits

If you are working with multiple packages you will probably want to filter the commits so that each package is processed with only the commits that are relevant to that package. To configure the logic for filtering records configure the plugin like this:

...
// This will filter the commits by the presence of `affects: pkg1, pkg2, pkg3...` string in the commit message.
// If you are using `cz-lerna-changelog` for commitizen, you will have exactly this in your commit messages.
wsr.analyzeCommits({
    isRelevant: (pkg, commit) => {
        if (commit.message.match(/affects:(.*)/)) {
            return RegExp.$1.split(",").map(n => n.trim()).filter(name => pkg.name === name).length;
        }
    }
})
...

Configuring the analyzeCommits plugin

We are using the @semantic-release/commit-analyzer plugin under the hood of our analyzeCommits plugin. If you need, you can pass the config to that semantic-release plugin by passing a commitAnalyzer config object. For all the config options visit @semantic-release/commit-analyzer

...
wsr.analyzeCommits({
    commitAnalyzer: {
        "preset": "angular",
        "releaseRules": [
            {"type": "docs", "scope":"README", "release": "patch"},
            {"type": "refactor", "release": "patch"},
            {"type": "style", "release": "patch"}
        ],
        "parserOpts": {
            "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"]
        }
    }
})
...