@-xun/release
v0.0.2
Published
A semantic-release fork with support for annotated tags and monorepos
Downloads
145
Maintainers
Readme
xrelease (@-xun/release)
This semantic-release fork slightly tweaks the original so that it can work with both polyrepos and monorepos (see below).
[!NOTE]
The only reason to use xrelease over semantic-release is if you are using an xscripts-powered project, your repository uses annotated tags, you need the bug fixes, or your repository is a monorepo. Otherwise, just use semantic-release.
Install
To install xrelease:
npm install --save-dev semantic-release@npm:@-xun/release
If you want to use a specific version of xrelease, provide its semver:
npm install --save-dev semantic-release@npm:@-xun/[email protected]
[!NOTE]
xrelease installations reuse the "semantic-release" name so that plugins with semantic-release as a peer dependency are able to recognize xrelease's presence.
Additional Features
xrelease offers a couple improvements over upstream:
Lightweight and Annotated Tag Support
Both lightweight and annotated tags are supported.
man git-tag
says:Annotated tags are meant for release while lightweight tags are meant for private or temporary object labels.
Support for Monorepos
Monorepo support is implemented via the existing tagFormat
configuration
option and the introduction of two new options: branchRangePrefix
and
gitLogOptions
.
[!WARNING]
These options have only been tested in release configuration files and might not be available via CLI.
Once properly configured, xrelease should be run once per package to be released, with the current working directory set to the root of each respective package.
For monorepos, if the current working directory does not contain the
repository's release configuration file, use --extends
to refer to its
location explicitly (e.g. --extends ../../release.config.js
); xrelease
supports using --extends
to load plugins from /node_modules/
directories
higher up in the repository tree. Further, a tool like Turbo can be used to
orchestrate package releases in dependency order.
[!NOTE]
See babel-plugin-tester's
release.config.js
(polyrepo), xscripts'srelease.config.js
(hybridrepo) or unified-utils'srelease.config.js
(monorepo) for complete functional examples of xrelease configurations in the wild.See the xscripts wiki or the git diff between this repo and upstream for technical details.
The extended configuration options are:
tagFormat
Type: string
Default: "v${version}"
The git tag format used by xrelease to create and identify releases. When
cutting a new release, its corresponding tag name will be generated using
tagFormat
.
tagFormat
is key to proper monorepo support since it dictates which existing
tags belong to the current package to be released and which belong to other
packages that should be filtered out.
To support a simple monorepo that uses "@"-syntax for its release tags (e.g.
[email protected]
), your release configuration might include:
// Tell xrelease what package-specific tags look like
tagFormat: `${cwdPackageName}@\${version}`;
[!CAUTION]
\${version}
(or${version}
in a non-template string literal) is a Lodash template variable while${cwdPackageName}
is a variable in a template string literal. That is: you are responsible for definingcwdPackageName
, while\${version}
is replaced by xrelease. Additionally, eachtagFormat
value must contain theversion
variable exactly once, and the wholetagFormat
value must compile to a valid git reference or commit-ish.
To refactor a polyrepo (that uses the standard semantic-release "v"-syntax for
its tags) into a monorepo (that uses the "@"-syntax for its tags), optionally
with a root package, use @-xun/scripts
's "renovate" command:
npx xscripts project renovate --task transmute-to-monorepo
.
gitLogOptions
Type: { paths?: string | string[], flags?: string | string[] }
Default: "v${version}"
The git log command line arguments used by xrelease to select
commits for further analysis. Currently, gitLogOptions
has two valid
options: flags
and paths
, which correspond to
git log <flags> -- <paths>
.
gitLogOptions
is key to proper monorepo support since it dictates what commits
belong to the current package to be released and which belong to other packages
that should be filtered out.
To support a monorepo attempting to release a new version of my-package-1
,
your release configuration might include:
gitLogOptions: {
// Tell xrelease to consider only commits that modify files under these paths
paths: [
':(exclude)../my-package-2',
':(exclude)../my-package-3',
':(exclude)../my-package-4'
];
}
In this example, we used exclusion pathspecs to create a blacklist of paths we didn't want instead of a whitelist of paths we do want. Either approach is viable depending on project structure; however, using exclusions ensures important changes that happen outside the package's root directory (such as changes to a shared unpublished library) are considered by xrelease when analyzing commits.
Note how the given pathspecs are relative to e.g.
/home/user/my-project/packages/my-package-1
. That's because xrelease should
always be run at the root of the package to be released.
The pathspec syntax we're using happens to work for releasing each of the other
packages as well, assuming they are all share the same parent directory, e.g.
/home/user/my-project/packages
. If we wanted to release my-package-2
next,
we would replace ':(exclude)../my-package-2'
with
':(exclude)../my-package-1'
and run xrelease again.
To support a polyrepo instead, your release configuration might include:
gitLogOptions: {
// Tell xrelease not to filter commits at all and instead consider everything
paths: [];
}
Or we could omit the gitLogOptions
object from release.config.js
entirely,
which would be equivalent.
Finally, you can use flags
to ensure git log
is invoked with certain
flags. For instance, we can tell xrelease to ignore all commits reachable by an
"initial" commit (including said commit itself). This could be useful if we
forked a large project with many thousands of commits (conventional or
otherwise) that should be ignored by the commit analyzer:
gitLogOptions: {
// Tell xrelease to filter commits created after the latest commit (including
// itself) with "[INIT]" suffixing its subject, a reference we acquired by
// running: `git log -1 --pretty=format:%H --fixed-strings --grep '\\[INIT]$'`
flags: ref ? [`^${ref}`] : [];
}
You can pass any flag that git log
understands.
branchRangePrefix
Type: string
Default: ""
The value that prefixes the names of relevant maintenance branches.
This is used internally by xrelease to generate the proper branches
configurations for maintenance branches that refer to particular packages in a
monorepo, and can be left undefined in a polyrepo.
[!CAUTION]
The
branchRangePrefix
string must only match maintenance branches! If you also define a non-maintenance branch with a name starting withbranchRangePrefix
, xrelease's behavior is undefined.
To support a simple monorepo that uses "[email protected]"-syntax for its
maintenance branch names (e.g. [email protected]
), your release
configuration might include:
// Tell xrelease to remove this string from maintenance branch names when
// resolving their respective ranges and channels
branchRangePrefix: `${cwdPackageName}@`,
branches: [
// Tell xrelease what package-specific maintenance branch names look like.
// Specifically: they must begin with `branchRangePrefix`
`${cwdPackageName}@+([0-9])?(.{+([0-9]),x}).x`,
'main'
]
To refactor a polyrepo (that uses the standard semantic-release "x.y.z"-syntax
for its maintenance branch names) into a monorepo (that uses the
"[email protected]"-syntax for its maintenance branch names), optionally with
a root package, use @-xun/scripts
's "renovate" command:
npx xscripts project renovate --task transmute-to-monorepo
.
Example
Putting the new configuration options together, we could use what follows to
release packages from the /home/user/my-project
hybridrepo (a monorepo with a
root package), which was formerly a polyrepo that we turned into a monorepo by
giving its root package.json
file a workspaces
key and creating
scoped aliases of existing maintenance branches and tags.
The my-project
repo contains the four packages my-package-1
through
my-package-4
under /home/user/my-project/packages/*
along with a
/home/user/my-project/package.json
file containing the name of the root
package (my-root-package
) and a /home/user/my-project/src
directory
containing the root package's source code.
We can push changes to main
, which is our primary release branch that
publishes to one or more packages' respective @latest
release channel
(the default for NPM projects). Or we can push changes to canary
, which will
publish to one or more packages' respective @canary
release channel. We
can also push changes to a [email protected]
branch, where package-name
represents the name of the monorepo package and x.y.z
represents the
maintenance branch range
.
// ./release.config.js
function makeConfiguration() {
const { cwdPackage } = getCurrentWorkingDirectoryPackageInformation();
const isCwdPackageTheRootPackage = isRootPackage(cwdPackage);
const gitLogPathspecs = getExcludedDirectoryPathspecs(cwdPackage);
const cwdPackageName = cwdPackage.json.name;
return {
// Tell xrelease what package-specific tags look like
tagFormat: `${cwdPackageName}@\${version}`,
// Tell xrelease to remove this string from maintenance branch names when
// resolving their respective ranges and channels
branchRangePrefix: `${cwdPackageName}@`,
gitLogOptions: {
// Tell xrelease to exclude commits from the other packages
paths: gitLogPathspecs
},
branches: [
// Tell xrelease what package-specific maintenance branch names look like.
// Specifically: they must begin with `branchRangePrefix`
`${cwdPackageName}@+([0-9])?(.{+([0-9]),x}).x`,
// If this is the root package, we could accept old-style branch names for
// the sake of compatibility. But it's usually better to just create new
// branch names aliases matching the `${cwdPackageName}@` pattern
//...(isCwdPackageTheRootPackage ? ['+([0-9])?(.{+([0-9]),x}).x'] : []),
'main',
{
name: 'canary',
channel: 'canary',
prerelease: true
}
],
plugins: [
// ...
]
};
}
module.exports = makeConfiguration();
Contributing
Consider contributing to upstream semantic-release instead.