internal/contributor-info/releasing.md
We're releasing this as a unified release with 6 packages total. We keep the version numbers in sync across all packages using unified versioning.
See Contributing
Always update CHANGELOG.md before running the release task. The release task reads the version from CHANGELOG.md and automatically creates a GitHub release from the changelog section.
main branch$update-changelog release for ordinary mainline
releases, or $react-on-rails-update-changelog release/rc/beta when the
PR must target release/X.Y.Z, to:
### [16.5.0] - 2026-03-08)release: collapse prior rc/beta sections of the same base version into the new stable section and deduplicate entries (rc/beta modes leave prior prerelease sections in place so users on an earlier RC can see what changed between RCs)SECURITY.md:
If a stable target lacks this section, the release task aborts before confirmation, tagging, or publication.
For a prerelease, the task warns and skips the GitHub release; after adding the section, create it with
sync_github_release.
rake release automatically creates a GitHub release if a changelog section exists -- no separate sync_github_release step neededThe simplest way to release is with no arguments -- the task reads the version from CHANGELOG.md:
# Recommended: reads version from CHANGELOG.md (requires step 1)
bundle exec rake release
# For a specific version (overrides CHANGELOG.md detection)
bundle exec rake "release[16.2.0]"
# For a pre-release version (note: use period, not dash)
bundle exec rake "release[16.2.0.beta.1]" # Creates npm package 16.2.0-beta.1
# For a release candidate
bundle exec rake "release[16.5.0.rc.0]"
# Dry run to test without publishing
bundle exec rake "release[16.2.0,true]"
# Override version policy checks (monotonic + changelog/bump consistency)
RELEASE_VERSION_POLICY_OVERRIDE=true bundle exec rake "release[16.2.0]"
bundle exec rake "release[16.2.0,false,true]"
Retry safety: Never drop the version argument when resuming an interrupted release. Retry the exact prerelease version, for example
bundle exec rake "release[17.0.0.rc.10]". From a prerelease checkout, an argument-less release fails closed unless the changelog advances the same release line to a newer prerelease. Stable promotion must use an explicit stable version and a matching non-empty changelog section.
When called with no arguments, rake release:
### [16.5.0])Dry runs use a temporary git worktree so version bumps and installs do not modify your current checkout.
rake release validates release-version policy before publishing:
### [X.Y.Z...]; not Unreleased), it maps to expected bump type:
Use override only when needed:
RELEASE_VERSION_POLICY_OVERRIDE=truebundle exec rake "release[..., ..., true]")Full argument list:
bundle exec rake "release[version,dry_run,override_version_policy,override_ci_status]"
version (optional): Version bump type or explicit version
patch, minor, major16.2.016.2.0.beta.1 (rubygem format with dots, converted to 16.2.0-beta.1 for NPM)dry_run (optional): true to preview changes without releasing (default: false)
override_version_policy (optional): true to override version policy checks (default: false)
override_ci_status (optional): global release-gate override (default: false). It is only for
an explicitly approved prerelease waiver under the active RC policy; never use it for a stable/final
promotion.
Stable/final promotion must not set
RELEASE_CI_STATUS_OVERRIDE=true, passoverride_ci_status=true, or use an accelerated asynchronous/deferred-gate bypass. Every unwaived final gate must pass. A narrowly scoped final waiver remains subject to the existing final-release policy, required evidence, and maintainer sign-off, and does not waive any other gate.
Environment variables:
VERBOSE=1 # Enable verbose logging (shows all output)
NPM_OTP=<code> # Provide NPM one-time password (reused for all NPM publishes)
RUBYGEMS_OTP=<code> # Provide RubyGems one-time password (reused for both gems)
RELEASE_VERSION_POLICY_OVERRIDE=true # Override release version policy checks
RELEASE_CI_EVALUATE_HEAD=true # Strictly evaluate the fetched exact release-source HEAD; not a waiver
RELEASE_CI_STATUS_OVERRIDE=true # DANGEROUS last-resort waiver for the release CI-status gate
GEM_RELEASE_MAX_RETRIES=<n> # Override max retry attempts (default: 3)
For this gate, HEAD means the fetched exact tip of the release-source branch that would be
tagged and published: origin/main for a mainline release or origin/release/X.Y.Z for a
release-branch cut/promotion. It never means an unpushed local checkout tip.
Normally, the gate walks back metadata-only commits (for example, a changelog/version commit) to the newest runtime-bearing commit. This is intentional: CI path filtering can attach no meaningful runtime suite to metadata-only commits, while the runtime-bearing commit is the one whose full suite establishes release health.
RELEASE_CI_EVALUATE_HEAD=true disables only that walkback. It still queries and enforces the
same CI gate at the exact fetched HEAD; it is a strict evaluation, not a waiver. It is appropriate
only for the narrow topology where GitHub attached complete workflows to the final release tip,
while the intermediate runtime SHA selected by normal walkback has zero usable runs.
| Normal walkback / exact HEAD evidence | Required action |
|---|---|
| Walked-back SHA has usable CI evidence | Let the normal gate decide. Do not set either variable. |
| Walked-back SHA has no usable runs; exact HEAD is pending | Wait for the linked exact-HEAD checks. They remain blocking. |
| Walked-back SHA has no usable runs; exact HEAD has failed checks | Fix or otherwise resolve the failures. They remain blocking. |
| Walked-back SHA has no usable runs; exact HEAD is completely healthy under the same stable/prerelease required-check rules | Re-run with RELEASE_CI_EVALUATE_HEAD=true; it evaluates that exact HEAD and still blocks on any failure. |
| Walked-back SHA has no usable runs; exact HEAD has no checks, unknown status, or an API failure | Fail closed. Wait for evidence or repair API/auth access; do not use strict HEAD without evidence. |
| Any case where a maintainer-approved waiver is truly required | RELEASE_CI_STATUS_OVERRIDE=true is the dangerous last resort, not a recovery default. |
Examples:
# Only after the task reports complete healthy exact-HEAD evidence, retry the explicit target version:
RELEASE_CI_EVALUATE_HEAD=true bundle exec rake "release[17.0.0.rc.10]"
Do not use RELEASE_CI_STATUS_OVERRIDE=true to substitute for pending, missing, failed, or
unknown exact-HEAD evidence. It waives the release CI-status gate and does not make CI healthy.
Examples:
bundle exec rake release # Auto-detect version; stable targets require changelog
bundle exec rake "release[patch]" # Bump patch version (16.1.1 → 16.1.2)
bundle exec rake "release[minor]" # Bump minor version (16.1.1 → 16.2.0)
bundle exec rake "release[major]" # Bump major version (16.1.1 → 17.0.0)
bundle exec rake "release[16.2.0]" # Set explicit version
bundle exec rake "release[16.2.0.beta.1]" # Set pre-release version (→ 16.2.0-beta.1 for NPM)
bundle exec rake "release[patch,true]" # Dry run
VERBOSE=1 bundle exec rake "release[patch]" # Release with verbose logging
NPM_OTP=123456 RUBYGEMS_OTP=789012 bundle exec rake "release[patch]" # Skip OTP prompts
The rake release task automatically:
npm login if needed)react_on_rails/lib/react_on_rails/version.rb (Ruby gem version)package.json files (npm package versions - converted from Ruby format)react-on-railsreact-on-rails-proreact-on-rails-pro-node-renderercreate-react-on-rails-appreact_on_railsreact_on_rails_proThe release task publishes 6 packages with unified versioning:
PUBLIC (npmjs.org + rubygems.org):
The task updates versions in all the following files:
Core package:
react_on_rails/lib/react_on_rails/version.rb (source of truth for all packages)package.json (root workspace)packages/react-on-rails/package.jsonGemfile.lock (root)react_on_rails/spec/dummy/Gemfile.lockPro package:
react_on_rails_pro/lib/react_on_rails_pro/version.rb (VERSION only, not PROTOCOL_VERSION)packages/react-on-rails-pro/package.json (+ dependency version)packages/react-on-rails-pro-node-renderer/package.jsonpackages/create-react-on-rails-app/package.jsonreact_on_rails_pro/Gemfile.lockreact_on_rails_pro/spec/dummy/Gemfile.lockNote:
react_on_rails_pro.gemspec dynamically references ReactOnRails::VERSIONreact-on-rails-pro NPM dependency is pinned to exact version (e.g., "react-on-rails": "16.2.0")Important: Use Ruby gem version format (no dashes) when passing versions to the rake task:
16.1.0, 16.2.0.beta.1, 16.0.0.rc.216.1.0-beta.1, 16.0.0-rc.2The task automatically converts Ruby gem format to npm semver format:
16.2.0.beta.1 -> npm: 16.2.0-beta.116.0.0.rc.2 -> npm: 16.0.0-rc.2CHANGELOG.md headers use RubyGems dot format (without v prefix):
### [16.5.0.rc.1] -- correct (matches gem version format)CHANGELOG.md compare links at the bottom of the file MUST use the v prefix to match git tags:
[16.5.0.rc.1]: https://github.com/shakacode/react_on_rails/compare/v16.4.0...v16.5.0.rc.1 -- correctrake release with no version, confirm the version detected from CHANGELOG.md. A stable checkout
may derive a patch candidate, but publication remains blocked until that version has a matching non-empty section.Verify the release on:
If the changelog was updated before release (recommended), verify the GitHub release was auto-created with the correct notes.
For a prerelease or historical release that predates the stable changelog gate, if the changelog was NOT updated before release, update it now. Current stable releases cannot reach this state because they abort before publication:
Option A - Use Claude Code (recommended):
Run $update-changelog 16.5.0 (using the already-released version) to analyze
commits, write entries, and automatically open a PR. Use
$react-on-rails-update-changelog instead when the catch-up PR must target
release/X.Y.Z. After the PR merges, pull the updated changelog and sync the
GitHub release:
git pull --rebase
bundle exec rake "sync_github_release[16.5.0]"
Option B - Manual (headers only, you must write entries):
bundle exec rake "update_changelog[16.5.0]"
# Write entries manually, then:
git commit -a -m 'Update CHANGELOG.md'
git push
bundle exec rake "sync_github_release[16.5.0]"
If the automatic GitHub release creation was skipped (e.g., CHANGELOG.md section was missing during release), you can create it manually after updating the changelog:
CHANGELOG.md with the published version sectionCHANGELOG.md# Stable
bundle exec rake "sync_github_release[16.5.0]"
# Prerelease
bundle exec rake "sync_github_release[16.5.0.rc.1]"
# Dry run
bundle exec rake "sync_github_release[16.5.0,true]"
sync_github_release reads release notes from the matching CHANGELOG.md section and creates/updates the GitHub release for the corresponding tag.
Before running the release command, verify:
GitHub CLI: Run gh auth login and ensure your account/token has write access to the repository (required for automatic GitHub release creation)
NPM authentication: Run npm whoami to confirm you're logged in
npm login for youRubyGems authentication: Ensure you have valid credentials for gem push
No uncommitted changes: Run git status to verify clean working tree
You'll need to enter OTP tokens when prompted:
react-on-rails to NPM (reused for subsequent NPM packages if valid)react_on_rails to RubyGems (reused for react_on_rails_pro if valid)You must be logged in and have publish permissions:
For public packages (npmjs.org):
npm login
For public gem (rubygems.org):
gem pushThe script automatically detects and switches Ruby versions when needed:
RUBY_VERSION_MANAGER environment variable (default: rvm)This task depends on the gem-release Ruby gem, which is installed via bundle install.
Before releasing to production, always preview with a dry run:
bundle exec rake "release[16.5.0,true]"
This uses a temporary git worktree to show exactly what would be updated without making any changes.
Always test with a dry run before actually releasing:
bundle exec rake "release[16.2.0,true]"
This shows you exactly what would be updated without making any changes.
If you see errors like "Access token expired" or "E404 Not Found" during NPM publish:
npm login to refresh your credentialsThe release script now checks NPM authentication at the start and will automatically run npm login if needed, so this issue will be caught and handled before any changes are made.
If the release fails partway through (e.g., during NPM publish):
Check what was published:
npm view [email protected]gem list react_on_rails -r -aIf the git tag was created but packages weren't published:
git tag -d vX.Y.Z && git push origin :vX.Y.Zgit reset --hard HEAD~1 && git push -fbundle exec rake "release[X.Y.Z]"If GitHub release creation fails after successful publishing:
gh auth login) or permissionsCHANGELOG.md has matching header ### [X.Y.Z]bundle exec rake "sync_github_release[X.Y.Z]"If some packages were published but not others:
cd packages/react-on-rails && pnpm version X.Y.Z && pnpm publish
cd ../react-on-rails-pro && pnpm version X.Y.Z && pnpm publish
gem release
pnpm publish -r will publish all packages where current version isn't published yet.Running bundle exec rake "release[X.Y.Z]" will create a commit that looks like this:
commit abc123...
Author: Your Name <[email protected]>
Date: Mon Jan 1 12:00:00 2024 -0500
Bump version to 16.2.0
diff --git a/react_on_rails/lib/react_on_rails/version.rb b/react_on_rails/lib/react_on_rails/version.rb
index 1234567..abcdefg 100644
--- a/react_on_rails/lib/react_on_rails/version.rb
+++ b/react_on_rails/lib/react_on_rails/version.rb
@@ -1,3 +1,3 @@
module ReactOnRails
- VERSION = "16.1.1"
+ VERSION = "16.2.0"
end
diff --git a/package.json b/package.json
index 2345678..bcdefgh 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-on-rails-workspace",
- "version": "16.1.1",
+ "version": "16.2.0",
...
}