Back to React On Rails

Releasing

internal/contributor-info/releasing.md

17.0.120.8 KB
Original Source

Install and Release

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.

Testing the Gem before Release from a Rails App

See Contributing

Release Process

1. Update the Changelog (BEFORE releasing)

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.

  1. Ensure all desired changes are merged to main branch
  2. Run the installed/shared $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:
    • Find merged PRs missing from the changelog
    • Add changelog entries under the appropriate category headings
    • Auto-compute the next version based on changes (breaking -> major, features -> minor, fixes -> patch) — skipped when an explicit version is provided
    • Stamp the version header (e.g., ### [16.5.0] - 2026-03-08)
    • For 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)
    • Automatically commit, push, and open a PR with the changelog changes
  3. For minor and major releases, add a commit to the changelog PR updating SECURITY.md:
    • "Current support window" table so supported version lines and cutoff dates match the release being shipped
    • "Last reviewed" date and, when applicable, "Next review due"
  4. Review the PR, verify the computed version, and merge

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.

Why changelog comes BEFORE the release

  • rake release automatically creates a GitHub release if a changelog section exists -- no separate sync_github_release step needed
  • The release task aborts a stable target if no matching non-empty section exists; prereleases warn and skip GitHub release creation
  • A premature version header (if release fails) is harmless -- you'll release eventually
  • A prerelease or historical release missing its changelog requires manual GitHub release synchronization

2. Run the Release Task

The simplest way to release is with no arguments -- the task reads the version from CHANGELOG.md:

bash
# 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:

  1. Reads the first versioned header from CHANGELOG.md (e.g., ### [16.5.0])
  2. Compares it to the current gem version
  3. If the changelog version is newer, prompts for confirmation and uses it
  4. If no new version is found from an already-stable checkout, derives a patch candidate; the stable changelog gate still blocks release until that version has a matching non-empty section. From a prerelease checkout, aborts with exact retry and stable-promotion guidance

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:

  • Target version must be greater than the latest tagged release.
  • If the versioned target changelog section exists (### [X.Y.Z...]; not Unreleased), it maps to expected bump type:
    • Breaking changes => major bump
    • Added/New Features/Features/Enhancements => minor bump
    • Fixed/Fixes/Bug Fixes/Security/Improved/Deprecated => patch bump
    • Other headings => no inferred bump level (consistency check is skipped)

Use override only when needed:

  • RELEASE_VERSION_POLICY_OVERRIDE=true
  • Or task arg override (bundle exec rake "release[..., ..., true]")

Full argument list:

bash
bundle exec rake "release[version,dry_run,override_version_policy,override_ci_status]"
  1. version (optional): Version bump type or explicit version

    • Bump types: patch, minor, major
    • Explicit: 16.2.0
    • Pre-release: 16.2.0.beta.1 (rubygem format with dots, converted to 16.2.0-beta.1 for NPM)
    • Empty (auto): use a newer changelog prerelease on the same release line; from an already-stable checkout, use a newer changelog version or derive a patch candidate that the stable changelog gate blocks until a matching non-empty section exists; otherwise abort with explicit retry guidance
  2. dry_run (optional): true to preview changes without releasing (default: false)

  3. override_version_policy (optional): true to override version policy checks (default: false)

  4. 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, pass override_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:

bash
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)

Release CI evidence and strict HEAD evaluation

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 evidenceRequired action
Walked-back SHA has usable CI evidenceLet the normal gate decide. Do not set either variable.
Walked-back SHA has no usable runs; exact HEAD is pendingWait for the linked exact-HEAD checks. They remain blocking.
Walked-back SHA has no usable runs; exact HEAD has failed checksFix 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 rulesRe-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 failureFail 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 requiredRELEASE_CI_STATUS_OVERRIDE=true is the dangerous last resort, not a recovery default.

Examples:

bash
# 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:

bash
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

3. What the Release Task Does

The rake release task automatically:

  1. Validates release prerequisites:
    • Checks for uncommitted changes (will abort if found)
    • Verifies NPM authentication (will run npm login if needed)
    • Requires a non-empty matching CHANGELOG.md section for stable targets; prereleases without one emit a warning, including during dry runs
    • Validates version policy (monotonic + changelog/bump consistency)
  2. Pulls latest changes from the repository
  3. Bumps version numbers in:
    • react_on_rails/lib/react_on_rails/version.rb (Ruby gem version)
    • All package.json files (npm package versions - converted from Ruby format)
    • Pro version files
  4. Updates Gemfile.lock files across the monorepo
  5. Commits, tags, and pushes all version changes
  6. Publishes to npm (requires 2FA token):
    • react-on-rails
    • react-on-rails-pro
    • react-on-rails-pro-node-renderer
    • create-react-on-rails-app
  7. Publishes to RubyGems (requires 2FA token):
    • react_on_rails
    • react_on_rails_pro
  8. Creates GitHub release from CHANGELOG.md (if the matching section exists)

What Gets Released

The release task publishes 6 packages with unified versioning:

PUBLIC (npmjs.org + rubygems.org):

  1. react-on-rails - NPM package
  2. react-on-rails-pro - NPM package
  3. react-on-rails-pro-node-renderer - NPM package
  4. create-react-on-rails-app - NPM package
  5. react_on_rails - RubyGem
  6. react_on_rails_pro - RubyGem

Version Synchronization

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.json
  • Gemfile.lock (root)
  • react_on_rails/spec/dummy/Gemfile.lock

Pro 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.json
  • packages/create-react-on-rails-app/package.json
  • react_on_rails_pro/Gemfile.lock
  • react_on_rails_pro/spec/dummy/Gemfile.lock

Note:

  • react_on_rails_pro.gemspec dynamically references ReactOnRails::VERSION
  • react-on-rails-pro NPM dependency is pinned to exact version (e.g., "react-on-rails": "16.2.0")

4. Version Format

Important: Use Ruby gem version format (no dashes) when passing versions to the rake task:

  • Correct: 16.1.0, 16.2.0.beta.1, 16.0.0.rc.2
  • Wrong: 16.1.0-beta.1, 16.0.0-rc.2

The task automatically converts Ruby gem format to npm semver format:

  • Ruby: 16.2.0.beta.1 -> npm: 16.2.0-beta.1
  • Ruby: 16.0.0.rc.2 -> npm: 16.0.0-rc.2

CHANGELOG.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 -- correct

5. During the Release

  1. When prompted for npm OTP, enter your 2FA code from your authenticator app
  2. When prompted for RubyGems OTP, enter your 2FA code
  3. If using rake 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.
  4. The script will automatically commit and push version bumps
  5. The script will automatically create a GitHub release (if CHANGELOG.md section exists)

6. After Release

  1. Verify the release on:

  2. If the changelog was updated before release (recommended), verify the GitHub release was auto-created with the correct notes.

  3. 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:

    bash
    git pull --rebase
    bundle exec rake "sync_github_release[16.5.0]"
    

    Option B - Manual (headers only, you must write entries):

    bash
    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]"
    

Syncing GitHub Releases Manually

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:

  1. Update CHANGELOG.md with the published version section
  2. Commit and push CHANGELOG.md
  3. Run:
bash
# 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.

Pre-Release Checklist

Before running the release command, verify:

  1. GitHub CLI: Run gh auth login and ensure your account/token has write access to the repository (required for automatic GitHub release creation)

  2. NPM authentication: Run npm whoami to confirm you're logged in

    • If not logged in, the release script will automatically run npm login for you
  3. RubyGems authentication: Ensure you have valid credentials for gem push

  4. No uncommitted changes: Run git status to verify clean working tree

Two-Factor Authentication

You'll need to enter OTP tokens when prompted:

  • Once for publishing react-on-rails to NPM (reused for subsequent NPM packages if valid)
  • Once for publishing react_on_rails to RubyGems (reused for react_on_rails_pro if valid)

Requirements

NPM Publishing

You must be logged in and have publish permissions:

For public packages (npmjs.org):

bash
npm login

RubyGems Publishing

For public gem (rubygems.org):

  • Standard RubyGems credentials via gem push

Ruby Version Management

The script automatically detects and switches Ruby versions when needed:

  • Supports: RVM, rbenv, asdf
  • Set via RUBY_VERSION_MANAGER environment variable (default: rvm)
  • Example: Pro dummy app requires Ruby 3.3.7, script auto-switches from 3.3.0

Dependencies

This task depends on the gem-release Ruby gem, which is installed via bundle install.

Testing with Dry Run

Before releasing to production, always preview with a dry run:

bash
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.

Troubleshooting

Dry Run First

Always test with a dry run before actually releasing:

bash
bundle exec rake "release[16.2.0,true]"

This shows you exactly what would be updated without making any changes.

NPM Authentication Issues

If you see errors like "Access token expired" or "E404 Not Found" during NPM publish:

  1. Your NPM token has expired (tokens now expire after 90 days)
  2. Run npm login to refresh your credentials
  3. Retry the release

The 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 Release Fails

If the release fails partway through (e.g., during NPM publish):

  1. Check what was published:

  2. If the git tag was created but packages weren't published:

    • Delete the tag: git tag -d vX.Y.Z && git push origin :vX.Y.Z
    • Revert the version commit: git reset --hard HEAD~1 && git push -f
    • Start over with bundle exec rake "release[X.Y.Z]"
  3. If GitHub release creation fails after successful publishing:

    • Fix GitHub auth (gh auth login) or permissions
    • Ensure CHANGELOG.md has matching header ### [X.Y.Z]
    • Rerun only: bundle exec rake "sync_github_release[X.Y.Z]"
  4. If some packages were published but not others:

    • You can manually publish the missing packages:
      bash
      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.

Version History

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",
   ...
}