Skip to content

Migrating from v1 to v2 ​

Relizy 2 releases version groups as one release, presented by group: one bump confirmation, one post per social platform, one PR comment with a table per group. It also fixes the social posts of the independent mode and cleans up the programmatic API.

Every project should read the CLI changes (confirmation in CI, push access, exit codes) the new commitlint rule (AI attribution in commits) and the formatting of the changelogs. The other changes only concern the projects using version groups, custom social templates, renamed or removed options, or the programmatic API.

Let your AI agent do it

With the MCP server of Relizy 2, ask your agent for the migrate-to-v2 prompt, or let it call the check_migration tool: it lists what to change in your config and your code, file by file and line by line, with the fix of each place.

Upgrade ​

bash
pnpm add -D relizy@^2

Then review the changes below, and check the next release with relizy release --dry-run.

ChangeWho is affectedAction
Single bump confirmationmonorepo.groupsNone
One social post per platformmonorepo.groups, independent modeReview the posts
PR commentmonorepo.groupsNone
Hooks run oncemonorepo.groups with social, twitter or slack hooksReview the hooks
Release titleCustom twitterMessage or social.twitter.templateUse {{releaseTitle}}
New version variableCustom social templates with monorepo.groups or independent modeUse {{releaseTitle}} or {{packageList}}
Every occurrence is replacedTemplates repeating a variableCheck the templates
Arrays replace the defaultsGlobal bump.dependencyTypes without dependenciesAdd dependencies to keep the v1 bumps
Changelog linter of commitlintcommitlint.codeBlocksLinterRename it changelogLinter
Private packagesmonorepo.includePrivates, --include-private, includePrivates of the APIUse privatePackages, --private-packages <mode>
Private app releasedA single package.json marked privateCheck relizy release --dry-run
Removed ignorePackageNamesmonorepo.ignorePackageNamesUse monorepo.ignored
Registry tokenpublish.tokenUse tokens.registry
Repository tokenrepo.tokenUse tokens.github or tokens.gitlab
Options never readnewVersion, publish.packagesRemove them
Removed publish.privatepublish.privateRemove it
AI attribution in commitsrelizy commitlintTurn off the attribution of the AI tools, or the rule
Version placeholderTemplates using {{version}}Use {{newVersion}}
Prereleases with onlyStableonlyStable with several versionsNone
AI on prereleasesai.providerRelease or ai.social on prereleasesNone, or set ai.onlyStable: false
Microsoft TeamsA script or a hook posting to Teams, an Office 365 connectorUse social.teams
Slack and Twitter announcementsA script or a hook posting some versions to SlackUse social.slack.announcements and exclude
Changelog of the postssocial.slack.changelogFormatRename it social.slack.changelog
Confirmation in CIrelizy release and relizy bump in CI, release() and bump() without a terminalAdd --yes (yes: true)
Push accessrelizy release that pushes, in CIGive the job the right to push
Exit code of bumprelizy bump --yes in scripts or CINone, or stop treating the exit code 1 as nothing to bump
Exit code of provider-release and socialrelizy provider-release, relizy social in CINone, or let the step fail
Cancelled promptsProgrammatic APICatch RelizyCancelError
Public APIProgrammatic APIReplace the functions and types no longer exported
Types entryrelizy/types importsImport from relizy
Release reportrelease() return value, buildReleaseSummary()Use the report
Version group resultsGroupBumpResult objectsAdd title

Version Groups ​

Single Bump Confirmation ​

The bump prompt lists the packages of every group, each under its title, and asks once, instead of once per group. Refusing it writes nothing, as before.

One Social Post per Platform ​

A release of several versions (several version groups, or several packages of an independent release) is posted once per platform, with a section per group, instead of once per group. The AI rewrites the posts in one call per platform. See One Release, Presented by Group for the posts and how they fit the limits of Twitter and Slack.

Give each group a title, shown in the posts, the PR comment and the bump prompt:

ts
export default defineConfig({
  monorepo: {
    groups: [
      { name: 'ecosystem', title: 'maz-ui', versionMode: 'selective', packages: ['packages/*'] },
      { name: 'tooling', title: 'Tooling', versionMode: 'independent', packages: ['tooling/*'] },
    ],
  },
})

A group with release: { social: false } is left out of the posts.

PR Comment ​

The PR comment has one table per group, titled with its title, its version and its version mode, instead of a single table, when several groups are released.

Hooks Run Once ​

The before, success and error hooks of social, twitter and slack run once per release, with the config of the release, instead of once per group with the config of the group. A function hook reading config.monorepo.versionMode or config.monorepo.packages in these hooks now gets the global config: read the release from its third parameter instead (see Hook Context).

Group Options ​

New options of a version group, all optional:

  • title: name of the group in the outputs of a release (default: name)
  • release.providerRelease and release.social: leave the group out of the provider releases or of the social posts
  • ai.providerRelease: create the provider releases of the group with or without AI, e.g. to limit the AI calls of an independent group (one provider release per package)

Social Templates ​

Release Title ​

The new {{releaseTitle}} variable is <name> <version> is out! when a single version is released (the root version, or a single package of an independent release), and New releases of <projectName> when several are. The default templates.twitterMessage uses it:

ts
// v1
'📣 {{projectName}} {{newVersion}} is out!\n\n{{changelog}}\n\n{{releaseUrl}}\n{{changelogUrl}}'
// v2
'📣 {{releaseTitle}}\n\n{{changelog}}\n\n{{releaseUrl}}\n{{changelogUrl}}'

Same text for the root version. In v1, an independent release posted "my-lib 3 packages is out!", or the project name with the version of its only package. Use {{releaseTitle}} in your own templates too.

The rich Slack message (without slackMessage template) uses 📣 {{releaseTitle}} as header.

New Version Variable ​

{{newVersion}} is the version released, and is empty when several versions are released, instead of 3 packages (or the root version only, with version groups). With version groups or the independent mode, replace it in your social templates with {{releaseTitle}}, or with the new {{packageList}} (@scope/[email protected], @scope/[email protected]) and {{packageCount}}:

ts
export default defineConfig({
  social: {
    slack: {
      // v1: '*{{projectName}} v{{newVersion}}* is out!\n\n{{changelog}}'
      template: '*{{releaseTitle}}*\n\n{{changelog}}\n\n{{packageList}}',
    },
  },
})

Every Occurrence Is Replaced ​

Every occurrence of a variable of a social template, of templates.changelogTitle and of templates.tagBody is replaced, instead of the first one only. A template repeating a variable now shows it each time. A tagBody repeating {{newVersion}} is refused: relizy could not read the version back from the tags.

Version Placeholder ​

{{version}} was never a variable of the social templates (some examples of the docs used it): replace it with {{newVersion}} or {{releaseTitle}}.

Prereleases with onlyStable ​

With onlyStable (the default of Slack), a post announcing several versions leaves its prereleases out, instead of posting every version as soon as one is stable. Nothing is posted when every version is a prerelease, as before.

Microsoft Teams ​

Relizy 2 posts the releases to Microsoft Teams (social.teams): a card per version, or one per release, with the texts and the buttons of each group or package. A script or a hook posting to Teams can be replaced by it:

  1. Map what the script posts to social.teams: its texts to title and message, its buttons to links, its texts per group or package to announcements, the versions it skips to exclude.
  2. Keep the URL of the workflow in the same variable: TEAMS_WEBHOOK_URL, or its base64 in TEAMS_WEBHOOK_URL_B64 (both also read with the RELIZY_ prefix).
  3. Check the cards with relizy social --dry-run, then remove the script and its job or hook: else the release is announced twice.

Microsoft retired the Office 365 connectors (https://*.webhook.office.com/...): a script posting to one fails. Create a Teams workflow instead, see Microsoft Teams.

Slack and Twitter Announcements ​

Relizy 1 announced every version on Slack and Twitter: a project announcing some versions only, or with its own texts, posted to Slack with a script or a hook. Relizy 2 does it:

  1. Map what the script posts to social.slack: the versions it announces to announcements ({ group } and { package } entries), their texts and links to the title, message and links of each entry, the versions it skips to exclude. On Twitter, exclude leaves versions out.
  2. Check the message with relizy social --dry-run, then remove the script and its job or hook: else the release is announced twice.

Without these options, the posts are unchanged.

Changelog of the Posts ​

social.slack.changelogFormat is renamed social.slack.changelog, as the option of Teams, with the same values (summary, full): relizy 2 ignores changelogFormat. Both take full, summary and none (or false, no changelog in the post).

ts
export default defineConfig({
  social: {
    slack: { enabled: true, changelog: 'full' },
  },
})

Config ​

Arrays Replace the Defaults ​

An array of the config replaces the default one, and the array of the config file when it comes from the CLI, the programmatic API or another layer of the config (the relizy key of package.json, a config it extends), instead of being concatenated with them. In v1, bump.dependencyTypes: ['peerDependencies'] resolved to ['peerDependencies', 'dependencies'], loadRelizyConfig({ overrides: { bump: { dependencyTypes: ['devDependencies'] } } }) could only add to the list of the config file.

If you set the global bump.dependencyTypes without dependencies, add it to keep bumping the packages for their dependencies, as v1 did:

ts
export default defineConfig({
  bump: {
    // v1 resolved ['peerDependencies'] to ['peerDependencies', 'dependencies']
    dependencyTypes: ['dependencies', 'peerDependencies'],
  },
})

Dependents Only Out of Range ​

v1 released a dependent on every release of its dependencies. A dependent without commits of its own is now released only when the range it was published with rejects the new version of its dependency (bump.dependents): with workspace:^, on a major, a minor in 0.x or a prerelease of the dependency, instead of every release. Set bump.dependents: 'always' to keep the behavior of v1, e.g. for a package that bundles its dependency.

Changelog Linter of commitlint ​

commitlint.codeBlocksLinter is renamed commitlint.changelogLinter, with the same values: with ESLint, it checks the whole changelog entry of the commit, not only its code blocks. The type CommitlintCodeBlocksLinter is renamed CommitlintChangelogLinter.

ts
import { defineConfig } from 'relizy'

export default defineConfig({
  commitlint: {
    changelogLinter: 'oxc',
  },
})

Private Packages ​

monorepo.includePrivates is renamed monorepo.privatePackages, with named values: ignore (default) or release. The --include-private flag of bump, changelog and release becomes --private-packages <mode>, and the includePrivates option of bump(), changelog() and release() becomes privatePackages.

ts
import { defineConfig } from 'relizy'

export default defineConfig({
  monorepo: {
    // includePrivates: true,
    privatePackages: 'release',
  },
})
bash
relizy release --private-packages release

Private App Released ​

A project with a single package.json marked "private": true (an app, an API) released nothing in v1: its package was skipped as a private package. It is now bumped, tagged, and gets its changelog and its provider release, but is never published, and no registry token is asked. Check what it does with relizy release --dry-run.

Removed ignorePackageNames ​

monorepo.ignorePackageNames, deprecated in v1, is removed: list the directories of these packages in monorepo.ignored, path globs matched as monorepo.packages. relizy 2 refuses to load a config that still sets it, rather than releasing the packages it left out.

ts
import { defineConfig } from 'relizy'

export default defineConfig({
  monorepo: {
    packages: ['packages/*'],
    // ignorePackageNames: ['@scope/legacy'],
    ignored: ['packages/legacy'],
  },
})

Repository Token ​

repo.token is removed: set the token of the provider releases and of the PR comment in tokens.github or tokens.gitlab, read from GITHUB_TOKEN, GH_TOKEN, GITLAB_TOKEN... by default, or with --token.

ts
import { defineConfig } from 'relizy'

export default defineConfig({
  // repo: { provider: 'gitlab', domain: 'gitlab.mycompany.com', repo: 'team/app', token: process.env.MY_TOKEN },
  repo: { provider: 'gitlab', domain: 'gitlab.mycompany.com', repo: 'team/app' },
  tokens: { gitlab: process.env.MY_TOKEN },
})

Registry Token ​

publish.token is removed: it read the same environment variables as tokens.registry (RELIZY_NPM_TOKEN, NPM_TOKEN, NODE_AUTH_TOKEN). Set the registry token in tokens.registry, or per registry in publish.registries[].token. --publish-token and the token option of publish() now set tokens.registry. In ReleaseOptions, token is only the git token.

ts
import { defineConfig } from 'relizy'

export default defineConfig({
  // publish: { token: process.env.MY_NPM_TOKEN },
  tokens: { registry: process.env.MY_NPM_TOKEN },
})

Options Never Read ​

The root newVersion option and publish.packages are removed: relizy never read them. Remove them from your config. To leave a package out of the release, use monorepo.ignored.

Removed Private Publish Option ​

publish.private is removed: relizy never read it, the private packages are never published. Remove it from your config, and set monorepo.privatePackages: 'release' to release them.

AI Attribution in Commits ​

relizy commitlint has a new rule, no-ai-attribution, on by default (error): it rejects the commits crediting an AI tool, with an AI co-author (Co-authored-by: Claude <[email protected]>), a Claude-Session: trailer or a "Generated with" line. Turn off the attribution in the settings of your AI tools, or turn off the rule:

ts
import { defineConfig } from 'relizy'

export default defineConfig({
  commitlint: {
    rules: { 'no-ai-attribution': 'off' },
  },
})

The changelogs no longer show the git trailers of the commit bodies (Co-authored-by:, Signed-off-by:, Closes #12...), only a BREAKING CHANGE: footer, and the AI tools are not credited as contributors.

Changelog Formatting ​

Without changelog.formatCmd, the formatter in the dependencies of the root package.json (Biome, Prettier, oxfmt, dprint, else ESLint) formats the changelogs written, and only them. If it fails, the release goes on with a warning. formatCmd replaces {{files}} by the paths of the changelogs written, so a command staging the files for lint-staged is no longer needed:

ts
import { defineConfig } from 'relizy'

export default defineConfig({
  changelog: {
    // v1: 'git add --all && pnpm lint-staged && git reset'
    formatCmd: 'eslint --fix {{files}}',
    // or false to keep the changelogs as written
  },
})

CLI ​

Exit Code of bump ​

relizy bump --yes exits with code 0 when there is nothing to bump, as relizy release does, instead of 1: a CI job running it no longer fails when no commit triggers a bump. Without --yes, it fails in CI, see Confirmation in CI. To run something only when a release happens, see Run Something Only When a Release Happens.

Confirmation in CI ​

Without an interactive terminal (CI), the confirmation of the new versions was taken as refused in v1: relizy release and relizy bump exited with code 0 without releasing anything. They now fail right away with code 1, --dry-run too, and tell to add --yes. The programmatic release() and bump() throw likewise without yes: true. The workflow generated by relizy init passes it.

bash
relizy release --yes

Push Access ​

In CI, Relizy prepares the clone of the job before a release: it fetches the history of a shallow clone, checks out the branch of a detached HEAD, and commits as the person who started the release when the job has no git identity (see CI/CD). Before publishing, relizy release then checks that the release commit and the tags can be pushed (git push --dry-run), --dry-run too, and fails with the fix otherwise: give the job the right to push (permissions: contents: write on GitHub Actions, a GITLAB_TOKEN on GitLab CI). The dry run of a pull request only warns, as its token is read-only. It also only warns when new commits were pushed to the pull request after it started, as their own run previews the release. See The Push Is Refused.

Exit Code of provider-release and social ​

relizy provider-release and relizy social exit with code 1 when a provider release or a post fails, instead of 0: a CI job running them now fails. A post that should not fail the job (an optional platform) can be disabled in the config, or the step allowed to fail in the CI (continue-on-error on GitHub Actions, allow_failure on GitLab CI).

New Output ​

The commands show one line per step and end with what they did, the details only with --log-level verbose (see Output). In CI, the logs of each step are in a collapsible group. The banner is only shown by relizy init. A script reading the output can set RELIZY_REPORTER=classic, which keeps the logs of the logger, close to the output of relizy 1, or better, read the report of the release.

Interactive Prompts ​

The prompts use @clack/prompts instead of @inquirer/prompts: Escape cancels a prompt as Ctrl+C does, and a cancelled prompt throws a RelizyCancelError (see Cancelled Prompts).

Release Summary ​

relizy release ends with a summary of the release: what it contains, what each step did, with the links of what it created (npm pages, provider releases, tweet, pull request), the AI rewrites and the install command. The PR comment of a release and the summary of the GitHub Actions run are this summary: check the PR comment if a tool reads it.

The new report of the release gives it to the CI: on GitHub Actions, by default and in dry-run too, the outputs of the step (released, status, version, tags, packages, branch, commit, dist_tags, urls, install, report, summary) and the summary of the run; with release.report, a markdown, JSON or dotenv file (GitLab CI).

relizy init Writes the CI Workflow ​

On GitHub or GitLab, relizy init writes the release workflow of the CI (.github/workflows/release.yml, or a release job aside the .gitlab-ci.yml), with the permissions and the tokens the config needs. --no-workflow skips it. See relizy init.

No AI Call in Dry Run ​

--dry-run no longer calls the AI provider (each call costs): the provider release and the social posts keep their raw text, and the output tells that the AI rewrite is simulated. To preview the AI output, add --ai-preview:

bash
relizy release --dry-run --ai-preview

AI on Prereleases ​

The AI no longer rewrites the prereleases (beta, canary...): their provider release notes and posts keep their raw changelog, without an AI call. To rewrite them as in v1, turn off ai.onlyStable, for every target or for one:

ts
import { defineConfig } from 'relizy'

export default defineConfig({
  ai: {
    providerRelease: { enabled: true, onlyStable: false },
  },
})

Programmatic API ​

Cancelled Prompts ​

A refused or cancelled prompt (bump confirmation, uncommitted changes, rewritten tag) throws a RelizyCancelError instead of calling process.exit(). Nothing was written, the commands do not run their error:* hooks for it and release() does not post a failed PR comment. The CLI still exits with code 0.

ts
import { isRelizyCancelError, release } from 'relizy'

try {
  await release()
}
catch (error) {
  if (!isRelizyCancelError(error)) {
    throw error
  }
}

Public API ​

relizy only exports the functions of the programmatic API and the types:

  • The commands: release(), bump(), changelog(), publish(), providerRelease(), social(), prComment(), commitlint()
  • The config: defineConfig(), loadRelizyConfig()
  • The errors: RelizyCancelError, isRelizyCancelError()
  • The commit linter: lintCommitMessage()
  • The types of the config, of the options and of the results (RelizyConfig, RelizyConfigOverrides, LoadRelizyConfigOptions, ReleaseOptions, BumpResult, ReleaseReport, HookContext...)

The other functions were internal steps of the commands, and are no longer exported:

RemovedInstead
createCommitAndTags(), pushCommitAndTags()release(), with its commit, gitTag and push options
planBumps(), confirmBump(), getPackages()bump(), with dryRun to preview it
formatTweetMessage(), postReleaseToTwitter(), formatSlackMessage(), postReleaseToSlack()social(), and the templates of the posts. A hook gets the packages and versions of the release in its release parameter
buildReleaseSummary()The report returned by release(), see Release report
generateChangelog(), generateMarkDown()changelog()
getDefaultConfig(), mergeTypes()loadRelizyConfig(), which returns the config with its defaults (config.types...)
isPrerelease()prerelease() of semver
init()relizy init
The helpers of git, npm, the tags, the versions, the dependencies, Slack and XThe commands, or the CLI

The types of these internal steps are no longer exported either (BumpPlan, PlannedBumps, ResolvedVersionGroup, VersionChange, OutOfRangePeer, ResolvedTags, ReleaseTag, InitOptions...). The types of the config, of the options and of the results stay, from relizy.

ts
// v1
const types = Object.keys(mergeTypes(relizyConfig.types, getDefaultConfig().types))

// v2
const config = await loadRelizyConfig()
const types = Object.keys(config.types)

Types Entry ​

The relizy/types entry is removed: import the types from relizy.

ts
// import type { RelizyConfig } from 'relizy/types'
import type { RelizyConfig } from 'relizy'

Release Report ​

release() returns the report of the release instead of nothing, and buildReleaseSummary() is removed. The summary of the release is written to the outputs and the summary of GitHub Actions, and with release.report, to a markdown, JSON or dotenv file (GitLab CI).

ts
const report = await release({ yes: true })

if (report.released) {
  console.log(`Released ${report.version}: ${report.tags.join(', ')}`)
}

Version Group Results ​

GroupBumpResult (bumpResult.groups) has a title: add it to the objects you build yourself, e.g. in tests.

Hook Context ​

The function hooks of the steps after the bump get a third parameter when the release is known: its bumpResult, and its release (the packages and versions by version group). Optional, nothing to change. See Hooks.

Fixes ​

No action needed:

  • The length of a tweet is counted as X counts it: every URL counts 23 characters, an emoji or a CJK character counts 2.
  • social() applies its safetyCheck option: relizy release no longer checks the social config twice.
  • publish.args is passed to the publish command: it was ignored.
  • bump.clean: false applies to relizy release too, not only to relizy bump.
  • The logLevel of the config applies when --log-level is not passed.
  • relizy publish --no-safety-check skips the safety check of the publication.
  • The messages of templates.commitMessage, commitBody and tagMessage are passed to git as they are: a quote, a $ or a backtick is no longer run by a shell.
  • The options given to bump(), changelog(), publish() and release() apply: capZeroMajor, outOfRangePeers, dependencyTypes, includeCommitBody, args, packageManager, safetyCheckTimeout and checkWorkspaceDependencies were ignored.
  • relizy release --provider creates the provider releases on that provider: it only applied to the safety check. relizy provider-release reads repo.provider.
  • The provider of a self-hosted repository (GitHub Enterprise, self-hosted GitLab) is read from its host when it names the provider, e.g. gitlab.example.com.
  • The GitLab releases are created with the job token of the CI job (CI_JOB_TOKEN) when no other GitLab token is set (tokens.gitlab, RELIZY_GITLAB_TOKEN, GITLAB_TOKEN, GITLAB_API_TOKEN): it was sent as a personal access token and refused. A job token cannot comment a merge request: the PR comment asks for a GITLAB_TOKEN.
  • The PR comment of a release run after the merge (on the push to main) comments the pull request it merged: without an open pull request of the branch, the pull request of the commit the release started from is commented. prComment({ dryRun: true }) returns false when no pull request is found.
  • relizy publish checks the access to the registry even when release.publish is false; relizy release only checks it when it publishes.
  • With publish.registries and without publish.registry, the packages are only published to the declared registries: they were also published to the registry of the npm config. A package that no entry covers still goes to the registry of the npm config.
  • The canary versions apply bump.capZeroMajor: a breaking change of 0.5.2 gives 0.6.0-0.canary-<sha>, as a release does. relizy release --canary --no-publish restores the versions of the package.json files.
  • A commitlint rule with a tuple without its number ('header-max-length': [2, 'always']) is an error of the config: the rule never reported anything.
  • The social posts: the changelog of a Slack template is in Slack's format (mrkdwn), the summary without AI keeps the lines of the changelog instead of cutting it by sentences, a template line holding only a URL that is not set is removed, and a Slack post of several versions lists each package once.
  • A repository without origin remote no longer shows the error of git.