A nnotated
T ags
A nd
M onorepos
This semantic-release fork makes some tiny tweaks to allow semantic-release to work with annotated tags and monorepos.
This fork is meant to be temporary. These are the pending PRs implementing ATAM functionality:
- Outwardly, nothing changes from the original semantic-release. It's a drop-in replacement!
- semantic-release-atam is NOT published as a new npm package, so you can
replace semantic-release without having to do
npm install --force
. master
branch is automatically rebased onto the latest release from upstream.- This allows semantic-release-atam to keep feature parity with semantic-release.
- Releases are maintained too, meaning you can install specific versions.
- Works with repos using annotated
tags out of
the box.
- This allows tags to be signed, for instance.
- Can be configured to work with monorepo/workspaces
setups.
- Borrows several configuration options from conventional-changelog.
- Better plugin loader resolution when using extended configuration.
- Monorepo packages can now load shared configuration from a single location.
- Maintenance branches can be easily configured to work with monorepos.
- Arguably less hacky than prior art.
NEVER install semantic-release and semantic-release-atam at the same time!
npm install --save-dev https://xunn.at/semantic-release-atam
If you want to use a specific version of semantic-release-atam, you can specify its release tag (without the prefix):
npm install --save-dev https://xunn.at/semantic-release-atam@19.0.5
Any valid commit-ish can be specified after the "@", not just version tags.
If you don't want to rely on xunn.at, you can also install the package from GitHub directly.
Being a temporary fork, semantic-release-atam is not published as a package, so
you cannot do npm install semantic-release-atam
.
This is because, to be an actual drop in replacement for semantic-release,
semantic-release-atam needs to use the name "semantic-release" (e.g. to satisfy
peer dependencies). Of course, only the real semantic-release can be installed
as "semantic-release", but we can get around that by using
"https://xunn.at/semantic-release-atam"
in lieu of a version in
package.json
:
{
...
"devDependencies": {
...
"semantic-release": "https://xunn.at/semantic-release-atam"
...
}
}
This is what the above command does for you automatically.
These instructions SHOULD NOT be used with Projector's pre-made configurations, since they handle all of this for you.
semantic-release-atam should be run once per package with each package's root as the working directory.
For example:
REPO_ROOT=...
NPM_CONFIG_USERCONFIG="$REPO_ROOT/.npmrc" NPM_TOKEN=$(cd $REPO_ROOT && npx --yes dotenv-cli -p NPM_TOKEN) GH_TOKEN=$(cd $REPO_ROOT && npx --yes dotenv-cli -p GITHUB_TOKEN) HUSKY=0 UPDATE_CHANGELOG=true GIT_AUTHOR_NAME=$(git config --global --get user.name) GIT_COMMITTER_NAME=$(git config --global --get user.name) GIT_AUTHOR_EMAIL=$(git config --global --get user.email) GIT_COMMITTER_EMAIL=$(git config --global --get user.email) npx --no-install semantic-release --extends "$REPO_ROOT/release.config.js"
Where semantic-release --extends "$REPO_ROOT/release.config.js"
loads a shared
release.config.js
file located at the repository's root.
When running semantic-release-atam on a normal (non-mono) repo,
release.config.js
would get picked up automatically. When running in a monorepo package's subfolder (e.g.packages/my-package-1
) however, the same config file can be (re)used via the--extends
CLI option.
This fork makes the gitLogOptions
option available in your
release.config.js
:
// Suppose process.cwd() returns /path/to/repo/packages/my-package-1
const targetPkgId = getPkgNameFromCwd();
module.exports = {
// semantic-release-atam will ignore tags that don't belong to my-package-1
tagFormat: `${targetPkgId}@\${version}`,
gitLogOptions: {
path: [":(exclude)../my-package-2", ":(exclude)../my-package-3", ":(exclude)../my-package-4"],
},
// ...
};
At the moment, gitLogOptions
has a single valid option: path: string | string[]
. Like with
conventional-changelog,
gitLogOptions.path
(which accepts one or more
paths/pathspecs,
including
exclusions)
can be used to make semantic-release-atam consider only those commits that
belong to the package, ignoring the others.
Note: it's usually better to filter via exclusion pathspecs than simple paths, which ensures important changes that happen outside the
packages/
directory are considered by semantic-release-atam and conventional-changelog.
Combined with tagFormat
, gitLogOptions.path
makes semantic-release-atam
flexible enough to work with most monorepo/workspace setups. Additionally,
monorepo maintenance branch support can be enabled via the new
branchRangePrefix
option.
Putting it all together:
// ./release.config.js
// Suppose process.cwd() returns /path/to/repo/packages/my-package-1
// Suppose __dirname equals /path/to/repo (meaning this file is at repo root)
const cwd = process.cwd();
const pathParts = cwd.replace(`${__dirname}/`, "").split("/");
// pathParts = [ 'packages', 'my-package-1' ]
if (pathParts.length < 2 || pathParts[0] != "packages") {
throw new Error(`assert failed: illegal cwd: ${cwd}`);
}
const targetPkgId = pathParts[1];
// targetPkgId = 'my-package-1'
// Returns an array of exclusion pathspecs, one for each package except the
// target package
const getExcludedDirs = (source, except) =>
readdirSync(source, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory() && dirent.name != except)
.map((dirent) => `:(exclude)${source}/${dirent.name}`);
module.exports = {
// Teach semantic-release-atam what our package-specific tags look like
// e.g.: my-package-1@1.0.0
tagFormat: `${targetPkgId}@\${version}`,
// ... and what our package-specific maintenance branches start with
// e.g.: my-package-1@1.x
branchRangePrefix: `${targetPkgId}@`,
gitLogOptions: {
// Tell semantic-release-atam to exclude commits from the other packages
path: getExcludedDirs("..", targetPkgId),
},
branches: [
// Teach semantic-release-atam what our maintenance branches look like. Must
// begin with `branchRangePrefix`
// e.g.: my-package-1@1.x
`${targetPkgId}@+([0-9])?(.{+([0-9]),x}).x`,
"main",
{
name: "canary",
channel: "canary",
prerelease: true,
},
],
//...
};
And, for package-specific changelog generation, a conventional-changelog
configuration
file
at conventional.config.js
that looks something like:
// ...
module.exports = {
options: {
lernaPackage: targetPkgId,
},
gitRawCommitsOpts: {
// ? Used to ignore changes in other packages
// ? See: https://github.com/sindresorhus/dargs#usage
"--": getExcludedDirs("..", targetPkgId),
},
};
And voilΓ ! π
The above can be used to (re)generate a complete CHANGELOG.md
file for any
monorepo package via CLI: npx conventional-changelog --outfile CHANGELOG.md --config ../../conventional.config.js --release-count 0 --skip-unstable
. This
can also be invoked via
@semantic-release/exec at build
time.
If going with conventional-changelog as your changelog generator, consider using the version patched to work properly with monorepos (PRs pending):
npm install --save-dev https://xunn.at/conventional-changelog-cli
This patched version also accepts workspace
as an alternative to the original
lernaPackage
option. The two options are functionally identical, except
workspace
accepts a path to a package directory where the lernaPackage
accepts only a package-id
(basename). Hence, workspace
allows changelogs to be generated for monorepos
that use a non-Lerna workspace/package structure.
// ...
module.exports = {
options: {
workspace: cwd,
},
gitRawCommitsOpts: {
// ? Used to ignore changes in other packages
// ? See: https://github.com/sindresorhus/dargs#usage
"--": getExcludedDirs("..", targetPkgId),
},
};
This fork is structured to be automatically rebased onto upstream releases when they occur. To facilitate this, care must be taken when committing changes to this repo. Specifically:
- The HEAD of the
master
branch MUST ALWAYS be therelease: bump version
commit. This allows the upstream synchronization script to do its job. - All changes should happen on the
master
branch. - Changes should be added to existing commits via
git commit --amend
and then force pushed viagit push --force
. If amending a pre-existing commit is not desirable for whatever reason, the new commit should be rebased under therelease: bump version
commit. - Never make custom releases or mess with the
atam@*
git tags. These are automatically managed by the upstream synchronization script.
For example, suppose we updated the README.md
file and want to commit the
changes:
git add README.md
git commit -m mergeme
git rebase -S -i HEAD~5 --no-verify
# Either make the mergeme commit a "fixup" to a pre-existing commit or
# reposition it to occur below HEAD
git push --force
Any changes between master
and the latest upstream release will be minted into
a new local release only after upstream makes a new release. Until then, any
changes will only be visible to those utilizing the master
branch directly.