Exploring the dry run as a rehearsal without side effects, how to read the generated version and changelog output, and validating release behavior between the main and staging branches before enabling an actual release.

In episode 8 we adjusted the commit analyzer rules; in episode 9 we built the lint and build gate. Now comes the most tense moment: before a real release touches the registry and creates tags, is the computed version correct?
The answer: dry run. It's a rehearsal that runs the entire semantic-release pipeline without side effects — the version is computed, the changelog is assembled, but nothing is actually released. This episode teaches how to set up a dry run, read its output, and validate branch behavior before a release.
A dry run follows the same flow as an actual release: takes the commits since the last tag, analyzes them with the commit analyzer, computes the version, then generates release notes. The difference: all side-effect stages are skipped:
Its single purpose: give an exact picture of what would happen, so you can correct course before it's too late.
Run it from the repository root:
npx semantic-release --dry-run --no-ciThe --no-ci flag tells semantic-release this is running outside CI. Without it, the tool refuses to run because it assumes it may only be executed in a CI environment.
$ npx semantic-release --dry-run --no-ci
[08:47:00] [semantic-release] › ℹ Running semantic-release version 24.1.0
[08:47:00] [semantic-release] › ✔ Loaded plugin analyzeCommits
[08:47:00] [semantic-release] › ℹ There are 3 commits on the main branch
since the last release (v1.2.0).
[08:47:00] [semantic-release] › ℹ Analyzing commit: feat(api): add retry
[08:47:00] [semantic-release] › ℹ Analyzing commit: fix(auth): refresh token
[08:47:00] [semantic-release] › ✔ The release type for the commit is minor
[08:47:00] [semantic-release] › ℹ The next release version is 1.3.0
[08:47:00] [semantic-release] › ✔ Generated release notes
[08:47:00] [semantic-release] › ℹ This run was started in dry-run mode, so
no release will be created.| Line in output | Meaning |
|---|---|
There are 3 commits ... since the last release | Number of commits analyzed |
Analyzing commit: feat(api): add retry | Commit currently being evaluated |
The release type for the commit is minor | Level of change from the commit |
The next release version is 1.3.0 | Final version that would be released |
Generated release notes | Changelog successfully assembled |
no commits since last release | Nothing worth releasing |
The two most important lines are release type and next release version. Release type explains the reason (minor because there's a feat), next release version shows the exact number (up from 1.2.0 to 1.3.0). If "no commits since last release" appears, it means there's no new release — and that's not an error.
Below the "Generated release notes" line, semantic-release shows the draft changelog that will be used for the GitHub release. Check two things: whether only commits that genuinely deserve it appear, and whether the version heading uses the correct tag format. A noisy changelog usually indicates releaseRules that are too loose.
Make the dry run an automatic status check on every pull request:
name: Release Check
on:
pull_request:
branches: [main, staging]
jobs:
dry-run:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- name: Semantic Release Dry Run
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release --dry-run --no-ciTwo important details: fetch-depth: 0 makes sure the entire history is checked out so tags and commits are readable, and permissions: contents: read is sufficient because a dry run writes nothing. Reviewers can open the "Semantic Release Dry Run" step log and read the version that would come out.
Tip
Combine the dry run with the commit analyzer you customized in episode 8. Every time the releaseRules change, the dry run result on the PR immediately shows the impact before those rules are actually used. That's feedback an actual release can't give.
The same behavior must be verified on both branches. The same feat(api): add retry commit produces different versions depending on the branch:
$ npx semantic-release --dry-run --no-ci
[08:50:12] [semantic-release] › ℹ The next release version is 1.3.0-rc.1
[08:50:12] [semantic-release] › ℹ This run was started in dry-run modeFrom staging comes 1.3.0-rc.1, from main comes 1.3.0. If you see an rc suffix appear on main or disappear on staging, the branch configuration in release.config.cjs is wrong and must be fixed before an actual release is enabled.
Warning
A dry run doesn't guarantee an actual release always succeeds. Things that only surface during a real release are npm publish token validation, write permissions on the registry, and GitHub release creation. So still test one actual release in a safe environment before trusting the whole pipeline.
| Mistake | Symptom | Solution |
|---|---|---|
Forgot --no-ci | Refused to run outside CI | Add the flag for local dry runs |
fetch-depth not set | Commits or tags not readable | Set fetch-depth: 0 on checkout |
| Misreading "no commits" | Thinking the pipeline is broken | Understand it means no release |
| Only testing one branch | Staging and main behavior differ | Dry run on both branches |
In episode 10 you:
fetch-depth: 0 and permissions: contents: read.staging and main behavior — the rc suffix must only appear on prereleases.In episode 11 we'll discuss Branch-based Release Conditions — how to write workflow conditions so a release only happens on the right branch. See you in episode 11!