This document aims to describe the current design of release-please and serve as a
primer for contributing to this library.
The release branch is the branch that you will create releases from. Most commonly this
is your repository's default branch (like main), but could also be a long
term support (LTS) or backport branch as well.
In the code, this is referred to as the targetBranch.
release-please is designed to propose releases via a pull request. release-please
will maintain a pull request ("release pull request") which proposes version bumps in
your code and appends release notes to your CHANGELOG.md. Releases are not created
until after this "release pull request" is merged to your release branch.
Maintainers can merge additional commits and release-please will update the existing
release pull request.
GitHub has a feature called a release which is the combination of a git reference (tag or
SHA) and release notes. release-please creates a GitHub release after a release pull
request is merged to your release branch.
In release-please terms, component is the name of a releasable unit. This could be a
library to publish to a package manager or an application that is deployed to a server.
Most commonly, a single GitHub repository contains code for a single component. In
other cases, a single GitHub repository could be a monorepo that contains code for
multiple components. release-please can handle both of these scenarios.
Semantic versioning is a specification that dictates how version numbers are formatted and incremented. This library assumes your version numbers are semantic versions.
For more information, see https://semver.org
Conventional commits is a specification for making commit messages machine-readable and
informing automation tools (like release-please) about the context of the commit.
For more information, see https://conventionalcommits.org
- A commit is merged/pushed to the release branch.
release-pleaseopens a release pull request.- A maintainer reviews/merges the release pull request.
release-pleasecreates a new GitHub release with the release notes (extracted from the release pull request).
Note: release-please is not responsible for publishing your package or application.
You can easily set up further automation to trigger from the creation of the release.
The general flow for opening a release pull request:
- Find the SHA of the latest released version for the component to be released
- Find all commits relevant to that component since the previous release
- Delegate to a language handler (
Strategy) to build the pull request, given the list of commits. - Create a pull request with the relevant code changes. Add the pending label
(defaults to
autorelease: pending) to the pull request.
More in-depth (including monorepo support):
- Build the manifest config
- Fetch and parse the manifest config/versions files OR
- Build the manifest in code
- Find the SHA of each of the latest released versions for each component
- Iterate through the latest GitHub releases (via GitHub GraphQL API)
- Fallback: iterate through GitHub tags on the repository
- Iterate backwards through commits until we've seen all the release SHAs or we hit a (configurable) max number of commits. Include fetching files for each of those commits
- Split commits for each component. Only commits that touch the directory of the component apply to that component.
- Run any plugin pre-configurators (for
Strategyconfigs). - For each component, build a candidate release pull request (if necessary)
- Run any plugin post-processors
- Optionally, combine multiple candidate release pull requests into a single pull request that will release all components together.
The general flow for creating a GitHub release:
- Find any merged release pull requests. We look for the pull request by label.
- For each merged release pull request, parse the pull request to determine the component version, and release notes.
- Create a new GitHub release that tags the SHA of the pull request's merge commit SHA. Use the parsed release notes as the GitHub release's body.
- Mark the pull request as tagged by adding the tagged label (defaults to
autorelease: tagged).
This library was originally built as a nodejs library and CLI tool. It does not use the
git CLI and instead opts to do its processing work in-memory and using the GitHub API.
All code paths that interact with GitHub are encapsulated in the
GitHub class. This design also helps us test the API calls and mock
out our API calls at a higher level than mocking the API JSON responses.
We use the @octokit/rest library to make calls to the GitHub API.
To authenticate, you can provide an access token or provide an existing authenticated
Octokit instance (for example, you can provide an Octokit instance from a
probot bot handler).
To actually open the pull request and update the code, we leverage the
code-suggester library.
Where possible, we would like to cache API calls so we limit our quota usage. An example of
this is the RepositoryFileCache which is a read-through cache
for fetching file contents/data from the GitHub API.
We have a concrete, core class Version which encapsulates a semantic
version.
A Version instance contains:
- semver major (number)
- semver minor (number)
- semver patch (number)
- pre-release version (string)
- build (string)
We define a VersioningStrategy interface that abstracts the
notion of how to increment a Version given a list of commits.
In the default case (DefaultVersioningStrategy):
- a breaking change will increment the semver major version
- a
featchange will increment the semver minor version - a
fixchange will increment the semver patch version
Note: VersioningStrategys are configurable independently of the language Strategy so
you can mix and match versioning strategies with language support.
release-please is highly extendable to support new languages and package managers. We
currently support 20+ different Strategy types many of which are community created and
supported.
We define a Strategy interface which abstracts the notion of what files to
update when proposing the next release version. In the most basic case
(Simple), we do not update any source files except the CHANGELOG.md.
Contributor note: Implementation-wise, most strategies inherit from the
BaseStrategy. This is not necessary, but it handles most of the common
behavior. If you choose to extend BaseStrategy, you only need to implement a single
buildUpdates() method (which files need to be updated).
Contributor note: If you implement a new Strategy, be sure to add a new corresponding
test to ensure we don't break it in the future.
The most common customization a Strategy makes is determining which standard files need to be
updated. For example, in a nodejs library, you will want to update the version entry in
your library's package.json (and package-lock.json if it exists).
We represent a file update via the Update interface. An Update contains the
path to the file needing an update, whether or not to create the file if it does not exist,
and how to update the file. The Updater interface is an abstraction that is
actually responsible for updating the contents of a file. An Updater implementation
generates updated content given the original file contents and a new version (or versions)
to update within that file.
Contributor note: If you implement a new Updater, be sure to add a new corresponding
test to ensure we don't break it in the future.
We define a ChangelogNotes interface which abstracts the notion of how
to build a CHANGELOG.md entry given a list of commits. The default implementation
(DefaultChangelogNotes), uses the
conventional-changelog-writer library to generate standardized release notes based on
the conventionalcommits.org specification.
We also have a second implementation that uses the GitHub changelog generator API.
release-please operates without a database of information and so it relies on GitHub as
the source of its information. Due to this, the release pull request is heavily formatted
and its structure is load-bearing.
The name of the HEAD branch that release-please creates its pull request from contains
important information that release-please needs.
As such, we implement a helper BranchName class that encapsulates that
data.
The HEAD branch name is not customizable at this time.
The pull request title can also contain important information that release-please needs.
As such, we implement a helper PullRequestTitle class that
encapsulates the data. This class contains the customization logic which allows users to
customize the pull request title.
The pull request body format is critical for release-please to operate as it includes
the changelog notes that will be included in the GitHub release.
For monorepos, it can also contain information for multiple releases so it must be parseable.
As such, we implement a helper PullRequestBody class that
encapsulates the data.
In release-please version 13, we integrated "manifest" releasers as a core part of the
library. Manifests were built to support monorepos, which can have many releasable
libraries in a single repository. The manifest is a JSON file that maps component path
<=> current release version. The manifest config file is a JSON file that maps component
path <=> component configuration. These files allow release-please to more easily track
multiple releasable libraries.
We highly recommend using manifest configurations (even for single library repositories) as
the configuration format is well defined (see schema) and it reduces the number of necessary
API calls. In fact, the original config options for release-please are actually converted
into a manifest configured release that only contains a single component.
Within a single Strategy, we treat the library as an independent entity -- the library
does not know or care that is part of a bigger monorepo.
Plugins provide an opportunity to break that encapsulation. They operate as pre-processors
and post-processors for the Strategy implementations.
We provide a ManifestPlugin interface that has 2 lifecycle hooks.
The first is the preconfigure hook, which allows making changes to a Strategy's
configuration. The second is the run (post-processor) hook, which allows making changes
to candidate release pull requests before they are created.
We provide JSON-schema representations of both the manifest config and manifest versions
files. These can be found in schemas/.
Contributor note: If you implement a new configuration option, make sure to update the JSON-schema to allow it.
We use a factory pattern to build all of our customizable components. This allows us to encapsulate the logic for building these components from configuration JSON and also makes it easier to mock for testing.
See src/factory and src/factories/.
Contributor note: If you implement a new configuration option, make sure to test that we correctly build the manifest configuration from the config JSON.
We heavily rely on unit testing to ensure release-please is behaving as expected. This is
a very complex codebase and we try to avoid breaking changes.
Contributor note: If you implement a new bugfix, please also add a new corresponding test to ensure we don't regress in the future.
We only consider the binary release-please CLI and the exported members from the index.ts
as part of the public interface. Other classes' interfaces are not considered part of the
public API and are subject to modification without requiring a new major release of
release-please.
Typescript/Javascript has limitations in its visibility scopes. If you choose to organize
source across many files, you cannot mark things as private if you use them in other files.
For example, you could have a file src/internal/private-class.ts which exports PrivateClass
for use as an implementation detail or for testability. An external developer could use
import {PrivateClass} from 'release-please/src/internal/private-class'; to access.
Contributor note: Do not make breaking changes to any exported entities from index.ts.
Doing so can break integrations. If the change is necessary, we will need to mark the
change as breaking and release a new major version.