From a1d16b28de5487116b0e62211d772bad707eac27 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 23 Jul 2026 13:26:29 +0300 Subject: [PATCH 1/2] Render mighost 0.5 ghost classification and add RBW_FORCE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ghost rows in db:migrate:status now come from Mighost::API.orphaned_migrations instead of per-version snapshot recovery: a superseded (re-timestamped) ghost renders with a calmer πŸͺ¦ status and a "≑ " badge pointing at its successor, a branchless deleted ghost shows "βœ‚ deleted in:", and ghosts dismissed or hidden via hide_superseded render as plain NO FILE. Live git/worktree recovery still covers versions without a stored snapshot, so fresh clones keep working with zero setup. Also fixes a stale branch badge leaking from one ghost row onto the next branchless one - the tag local was never reset per iteration, which was unreachable while mighost always guessed a branch. RBW_FORCE=1 forces formatting past NO_COLOR, CI, non-tty, and LLM agent detection; an explicit RBW_PLAIN=1 still wins. --- CHANGELOG.md | 27 ++++ README.md | 7 +- lib/railbow.rb | 2 + lib/railbow/params.rb | 4 + lib/railbow/tasks/migrate_status.rake | 143 ++++++++++++++--- spec/railbow/tasks/migrate_status_spec.rb | 185 +++++++++++++++++++++- spec/railbow_spec.rb | 65 ++++++++ 7 files changed, 408 insertions(+), 25 deletions(-) create mode 100644 spec/railbow_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index fa36bdf..29a5af7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Ghost rows in `db:migrate:status` now surface mighost 0.5's classification: + a superseded ghost (re-timestamped migration) shows a calmer πŸͺ¦ status with + a `≑ ` badge pointing at its successor, and a ghost deleted with no + surviving branch shows `βœ‚ deleted in:`. Branch badges are unchanged and + take precedence right after supersession. +- `RBW_FORCE=1` forces Railbow formatting past every auto-detection + (`NO_COLOR`, CI, piped output, LLM agent detection). An explicit + `RBW_PLAIN=1` still wins. + +### Changed + +- Ghost data now comes from `Mighost::API.orphaned_migrations`, so + `db:migrate:status` honors mighost dismissals and `hide_superseded` for the + first time: a dismissed or hidden ghost renders as plain `NO FILE` instead + of πŸ‘». Live git/worktree recovery still kicks in for versions without a + stored snapshot, so fresh clones keep working with zero setup. + +### Fixed + +- A ghost row without a branch no longer inherits the previous ghost row's + branch badge (a stale local leaked across loop iterations; unreachable + before mighost 0.5 made branchless ghosts the normal case). + ## [0.2.0] - 2026-07-23 ### Added diff --git a/README.md b/README.md index 4dfefad..b2de551 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Out of the box you get: - **Time filtering** - only the last 70 days shown by default (`since: 70d`) - **Your migrations highlighted** - rows authored by you are visually distinct -If the [mighost](https://github.com/amberpixels/mighost) gem is installed (`gem "mighost", group: [:development, :test]`), migrations whose files were deleted (e.g. after switching branches) show up with a πŸ‘» status and their recovered name and origin branch instead of a bare `********** NO FILE **********` row - Railbow talks to it through the stable `Mighost::API`. +If the [mighost](https://github.com/amberpixels/mighost) gem is installed (`gem "mighost", group: [:development, :test]`), migrations whose files were deleted (e.g. after switching branches) show up with a πŸ‘» status and their recovered name instead of a bare `********** NO FILE **********` row - Railbow talks to it through the stable `Mighost::API`. Each ghost row carries the most informative badge mighost can provide: `≑ ` when the migration was re-timestamped and lives on under another version (shown with a calmer πŸͺ¦ status), `βŒ₯ ` when a branch still holds the file, or `βœ‚ deleted in:` pointing at the commit that removed it. Ghosts dismissed via `mighost:dismiss` (or hidden by `hide_superseded`) render as plain `NO FILE`. ### `rails db:migrate:down` @@ -176,6 +176,7 @@ Every option can also be set via `RBW_*` environment variables, which override c | Variable | Example | Description | |---|---|---| | `RBW_PLAIN` | `1` | Disable all formatting | +| `RBW_FORCE` | `1` | Force formatting even when piped, in CI, or run by an LLM agent (`RBW_PLAIN=1` still wins) | | `RBW_SINCE` | `2mo`, `70d`, `1y`, `all` | Filter migrations by time period | | `RBW_DATE` | `full`, `rel`, `short`, `custom(%b %d)` | Date display format | | `RBW_GIT` | `author:me,diff,mask:auto` | Git integration options | @@ -221,6 +222,10 @@ Formatting auto-disables when: - Output is piped or redirected (non-TTY) - Running inside an LLM agent (`CLAUDECODE` env var) +`RBW_FORCE=1` overrides all of the auto-detection above (useful for capturing +formatted output to a file, or letting an agent inspect the real rendering). +An explicit `RBW_PLAIN=1` always wins over `RBW_FORCE`. + ## Requirements - Ruby >= 3.1.0 diff --git a/lib/railbow.rb b/lib/railbow.rb index 15ac862..6058be7 100644 --- a/lib/railbow.rb +++ b/lib/railbow.rb @@ -9,8 +9,10 @@ class Error < StandardError; end # Returns true when Railbow formatting should be disabled. # Checks for explicit opt-out, standard conventions, CI, and LLM agents. + # RBW_PLAIN always wins; RBW_FORCE beats every auto-detection below it. def self.plain? return true if Params.plain? + return false if Params.force? return true if ENV.key?("NO_COLOR") return true if ENV.key?("CLAUDECODE") return true if ENV.key?("CI") diff --git a/lib/railbow/params.rb b/lib/railbow/params.rb index 36e5e94..4b45876 100644 --- a/lib/railbow/params.rb +++ b/lib/railbow/params.rb @@ -71,6 +71,10 @@ def plain? truthy?(ENV["RBW_PLAIN"]) end + def force? + truthy?(ENV["RBW_FORCE"]) + end + def since (ENV["RBW_SINCE"] || Config.load["since"] || "all").strip.downcase end diff --git a/lib/railbow/tasks/migrate_status.rake b/lib/railbow/tasks/migrate_status.rake index 38b9869..011bb75 100644 --- a/lib/railbow/tasks/migrate_status.rake +++ b/lib/railbow/tasks/migrate_status.rake @@ -12,8 +12,113 @@ require_relative "../logo" # db:migrate:status and db:migrate:status: tasks. module Railbow module MigrateStatusFormatter + # Ghost migration data normalized for rendering, whether it came from + # mighost's orphan classification or a live snapshot recovery. + class GhostRow + attr_reader :filename, :branch_name, :source, :superseded_by, :deleted_in_sha, + :author_name, :author_email, :content + + def initialize(filename: nil, branch_name: nil, source: nil, superseded_by: nil, + deleted_in_sha: nil, author_name: nil, author_email: nil, content: nil) + @filename = filename + @branch_name = branch_name + @source = source + @superseded_by = superseded_by + @deleted_in_sha = deleted_in_sha + @author_name = author_name + @author_email = author_email + @content = content + end + end + private + def mighost_attr(obj, name) + obj.respond_to?(name) ? obj.public_send(name) : nil + end + + def mighost_snapshot_content(version) + Mighost::API.find_snapshot(version)&.content + rescue + nil + end + + def load_ghost_rows(versions, with_content: false) + # Detect once: OrphanedMigration carries the classification (supersession, + # deletion commit) that a bare snapshot doesn't, and already honors + # dismissals and hide_superseded. + orphans = begin + Mighost::API.orphaned_migrations.to_h { |o| [o.version.to_s, o] } + rescue + return {} + end + + rows = {} + versions.each do |v| + # Absent from detect = deliberately suppressed (dismissed, or superseded + # with hide_superseded on) - render as plain NO FILE, don't re-recover. + next unless (orphan = orphans[v]) + + if orphan.filename && !orphan.filename.empty? + rows[v] = GhostRow.new( + filename: orphan.filename, + branch_name: orphan.branch_name, + source: mighost_attr(orphan, :source), + superseded_by: mighost_attr(orphan, :superseded_by), + deleted_in_sha: mighost_attr(orphan, :deleted_in_sha), + author_name: mighost_attr(orphan, :author_name), + author_email: mighost_attr(orphan, :author_email), + content: with_content ? mighost_snapshot_content(v) : nil + ) + else + # Detect reads stored snapshots only. A version it lists without a + # filename has no snapshot yet, so fall back to live git/worktree + # recovery - keeps fresh clones working with zero setup. + snapshot = begin + Mighost::API.find_or_recover_snapshot(v) + rescue + nil + end + next unless snapshot&.filename && !snapshot.filename.empty? + + rows[v] = GhostRow.new( + filename: snapshot.filename, + branch_name: snapshot.branch_name, + source: mighost_attr(snapshot, :source), + superseded_by: api_superseded_by(v), + deleted_in_sha: mighost_attr(snapshot, :deleted_in_sha), + author_name: mighost_attr(snapshot, :author_name), + author_email: mighost_attr(snapshot, :author_email), + content: with_content ? snapshot.content : nil + ) + end + end + rows + end + + def api_superseded_by(version) + return nil unless Mighost::API.respond_to?(:superseded_by) + + Mighost::API.superseded_by(version) + rescue + nil + end + + # One tag slot per ghost row; most informative wins. + def ghost_tag(ghost) + if ghost.superseded_by + "\e[38;5;245m≑ #{ghost.superseded_by}\e[38;5;217m" + elsif ghost.branch_name + if ghost.source == "worktree" + "\e[38;5;222mβŒ₯β‚œ#{ghost.branch_name}\e[38;5;217m" + else + "\e[38;5;222mβŒ₯ #{ghost.branch_name}\e[38;5;217m" + end + elsif ghost.deleted_in_sha + "\e[38;5;245mβœ‚ deleted in:#{ghost.deleted_in_sha[0, 8]}\e[38;5;217m" + end + end + def git_migration_authors(migrate_dir) output, _status = Railbow::GitUtils.capture2( "log", "--format=COMMIT:%aN\t%aE", "--diff-filter=AR", "--name-status", "--", migrate_dir @@ -304,6 +409,9 @@ module Railbow RBW_PLAIN=1 Disable Railbow formatting (plain Rails output) + RBW_FORCE=1 Force Railbow formatting even when piped, in CI, + or called by an LLM agent (RBW_PLAIN=1 still wins) + RBW_HELP=1 Show this help message \e[2mAuto-disabled when piped, in CI, or when called by an LLM agent.\e[0m @@ -428,19 +536,12 @@ module Railbow uncommitted_files.each { |f| branch_origins[f] ||= current_branch } end - # Load mighost snapshots for "NO FILE" migrations (if mighost gem is available) + # Load mighost ghost data for "NO FILE" migrations (if mighost gem is available) mighost_snapshots = {} mighost_available = defined?(Mighost::API) && Mighost.enabled? if mighost_available no_file_versions = db_list.select { |_, _, n| n.include?("NO FILE") }.map { |_, v, _| v.to_s } - no_file_versions.each do |v| - snapshot = begin - Mighost::API.find_or_recover_snapshot(v) - rescue - nil - end - mighost_snapshots[v] = snapshot if snapshot&.filename && !snapshot.filename.empty? - end + mighost_snapshots = load_ghost_rows(no_file_versions, with_content: tables_enabled) if no_file_versions.any? end # Build columns @@ -501,29 +602,25 @@ module Railbow ghost_snapshot = name.include?("NO FILE") ? mighost_snapshots[version.to_s] : nil if name.include?("NO FILE") && ghost_snapshot ghost_rows << idx - # Mighost recovered this ghost migration β€” show πŸ‘» status + name + branch badge - colored_status = "πŸ‘»" + # Mighost recovered this ghost migration β€” show ghost status + name + badge. + # A superseded ghost lives on under another version: stale bookkeeping, + # not a lost migration, so it gets a calmer glyph. + colored_status = ghost_snapshot.superseded_by ? "πŸͺ¦" : "πŸ‘»" ghost_name = ghost_snapshot.filename .sub(/\A\d+_/, "") # strip version prefix .sub(/\.rb\z/, "") # strip extension .tr("_", " ") .gsub(/\b\w/, &:upcase) # titleize - if ghost_snapshot.branch_name - branch_tag = if ghost_snapshot.respond_to?(:source) && ghost_snapshot.source == "worktree" - "\e[38;5;222mβŒ₯β‚œ#{ghost_snapshot.branch_name}\e[38;5;217m" - else - "\e[38;5;222mβŒ₯ #{ghost_snapshot.branch_name}\e[38;5;217m" - end - end - if branch_tag && name_col_width - tag_width = formatter.display_width(formatter.strip_ansi(branch_tag)) + ghost_badge = ghost_tag(ghost_snapshot) + if ghost_badge && name_col_width + tag_width = formatter.display_width(formatter.strip_ansi(ghost_badge)) available = name_col_width - tag_width - 2 ghost_name = formatter.truncate_str(ghost_name, available) name_width = formatter.display_width(ghost_name) padding = name_col_width - name_width - tag_width - display_name = "#{ghost_name}#{" " * [padding, 2].max}#{branch_tag}" - elsif branch_tag - display_name = "#{ghost_name} #{branch_tag}" + display_name = "#{ghost_name}#{" " * [padding, 2].max}#{ghost_badge}" + elsif ghost_badge + display_name = "#{ghost_name} #{ghost_badge}" else display_name = ghost_name end diff --git a/spec/railbow/tasks/migrate_status_spec.rb b/spec/railbow/tasks/migrate_status_spec.rb index cf3b166..f8f33c3 100644 --- a/spec/railbow/tasks/migrate_status_spec.rb +++ b/spec/railbow/tasks/migrate_status_spec.rb @@ -2,11 +2,29 @@ require "spec_helper" require "active_record" +require "mighost" +require "stringio" load File.expand_path("../../../lib/railbow/tasks/migrate_status.rake", __dir__) RSpec.describe Railbow::MigrateStatusFormatter do - subject(:helper) { Class.new { include Railbow::MigrateStatusFormatter }.new } + subject(:helper) { helper_class.new } + + let(:helper_class) do + Class.new do + include Railbow::MigrateStatusFormatter + + attr_accessor :migration_connection_pool + end + end + + def ghost_row(**attrs) + Railbow::MigrateStatusFormatter::GhostRow.new(**attrs) + end + + def orphan(version:, **attrs) + Mighost::OrphanDetector::OrphanedMigration.new(version: version, **attrs) + end describe "#apply_branch_mask" do it "extracts the first capture group of the mask" do @@ -25,4 +43,169 @@ expect(helper.send(:apply_branch_mask, "feature/foo", "([")).to eq("feature/foo") end end + + describe "#ghost_tag" do + it "prefers the superseded badge over branch and deletion" do + tag = helper.send(:ghost_tag, ghost_row( + superseded_by: "20260105130000", + branch_name: "origin/apples", + deleted_in_sha: "aabbccddeeff" + )) + expect(tag).to include("≑ 20260105130000") + expect(tag).not_to include("βŒ₯") + end + + it "shows the branch badge when there is no superseder" do + tag = helper.send(:ghost_tag, ghost_row(branch_name: "origin/apples", deleted_in_sha: "aabbccddeeff")) + expect(tag).to include("βŒ₯ origin/apples") + end + + it "marks worktree branches with the worktree glyph" do + tag = helper.send(:ghost_tag, ghost_row(branch_name: "apples", source: "worktree")) + expect(tag).to include("βŒ₯β‚œapples") + end + + it "falls back to the deletion commit with a short sha" do + tag = helper.send(:ghost_tag, ghost_row(deleted_in_sha: "aabbccddeeff")) + expect(tag).to include("βœ‚ deleted in:aabbccdd") + end + + it "returns nil when there is nothing to say" do + expect(helper.send(:ghost_tag, ghost_row)).to be_nil + end + end + + describe "#load_ghost_rows" do + before { allow(Mighost::API).to receive(:superseded_by).and_return(nil) } + + it "builds rows from detect results, carrying the classification" do + allow(Mighost::API).to receive(:orphaned_migrations).and_return([ + orphan(version: "20260101110000", filename: "20260101110000_add_apples.rb", + branch_name: nil, superseded_by: "20260105130000", deleted_in_sha: "aabbccddeeff") + ]) + + rows = helper.send(:load_ghost_rows, ["20260101110000"]) + expect(rows.keys).to eq(["20260101110000"]) + expect(rows["20260101110000"].superseded_by).to eq("20260105130000") + expect(rows["20260101110000"].deleted_in_sha).to eq("aabbccddeeff") + end + + it "recovers live when detect lists a version without a snapshot filename" do + allow(Mighost::API).to receive(:orphaned_migrations).and_return([ + orphan(version: "20260101110000") + ]) + snapshot = Mighost::Snapshot.new( + version: "20260101110000", + filename: "20260101110000_add_bananas.rb", + branch_name: "origin/bananas" + ) + allow(Mighost::API).to receive(:find_or_recover_snapshot).with("20260101110000").and_return(snapshot) + allow(Mighost::API).to receive(:superseded_by).with("20260101110000").and_return("20260109090000") + + rows = helper.send(:load_ghost_rows, ["20260101110000"]) + expect(rows["20260101110000"].filename).to eq("20260101110000_add_bananas.rb") + expect(rows["20260101110000"].branch_name).to eq("origin/bananas") + expect(rows["20260101110000"].superseded_by).to eq("20260109090000") + end + + it "does not recover versions absent from detect (dismissed or hidden)" do + allow(Mighost::API).to receive(:orphaned_migrations).and_return([]) + expect(Mighost::API).not_to receive(:find_or_recover_snapshot) + + rows = helper.send(:load_ghost_rows, ["20260101110000"]) + expect(rows).to be_empty + end + + it "returns no rows when detect itself fails" do + allow(Mighost::API).to receive(:orphaned_migrations).and_raise(StandardError) + expect(helper.send(:load_ghost_rows, ["20260101110000"])).to eq({}) + end + + it "loads snapshot content only when requested" do + allow(Mighost::API).to receive(:orphaned_migrations).and_return([ + orphan(version: "20260101110000", filename: "20260101110000_add_apples.rb") + ]) + snapshot = Mighost::Snapshot.new(version: "20260101110000", + filename: "20260101110000_add_apples.rb", content: "create_table :apples") + allow(Mighost::API).to receive(:find_snapshot).with("20260101110000").and_return(snapshot) + + rows = helper.send(:load_ghost_rows, ["20260101110000"], with_content: true) + expect(rows["20260101110000"].content).to eq("create_table :apples") + + expect(helper.send(:load_ghost_rows, ["20260101110000"])["20260101110000"].content).to be_nil + end + end + + describe "#migrate_status ghost rendering" do + let(:no_file_name) { "********** NO FILE **********" } + let(:db_list) do + [ + ["up", "20260101000001", no_file_name], + ["up", "20260101000002", no_file_name] + ] + end + + before do + allow(Railbow).to receive(:plain?).and_return(false) + allow(Railbow::Params).to receive(:since).and_return("all") + + schema_migration = double(table_exists?: true) + db_config = double(database: "testdb") + migration_context = double(migrations_status: db_list, migrations: []) + helper.migration_connection_pool = + double(schema_migration: schema_migration, db_config: db_config, migration_context: migration_context) + + allow(Mighost).to receive(:enabled?).and_return(true) + allow(Mighost::API).to receive(:superseded_by).and_return(nil) + end + + def render_status + captured = StringIO.new + original = $stdout + $stdout = captured + helper.migrate_status + captured.string + ensure + $stdout = original + end + + it "does not leak the previous ghost's branch badge onto a branchless ghost" do + allow(Mighost::API).to receive(:orphaned_migrations).and_return([ + orphan(version: "20260101000001", filename: "20260101000001_add_apples.rb", + branch_name: "origin/apples"), + orphan(version: "20260101000002", filename: "20260101000002_add_bananas.rb", + branch_name: nil) + ]) + + lines = render_status.lines + first = lines.find { |l| l.include?("Add Apples") } + second = lines.find { |l| l.include?("Add Bananas") } + expect(first).to include("βŒ₯ origin/apples") + expect(second).not_to include("βŒ₯") + end + + it "renders a superseded ghost with the calm glyph and successor badge" do + allow(Mighost::API).to receive(:orphaned_migrations).and_return([ + orphan(version: "20260101000001", filename: "20260101000001_add_apples.rb", + superseded_by: "20260101000002"), + orphan(version: "20260101000002", filename: "20260101000002_add_bananas.rb", + branch_name: "origin/bananas") + ]) + + output = render_status + line = output.lines.find { |l| l.include?("Add Apples") } + expect(line).to include("πŸͺ¦") + expect(line).to include("≑ 20260101000002") + expect(line).not_to include("πŸ‘»") + end + + it "renders ghosts suppressed by mighost as plain NO FILE without recovery" do + allow(Mighost::API).to receive(:orphaned_migrations).and_return([]) + expect(Mighost::API).not_to receive(:find_or_recover_snapshot) + + output = render_status + expect(output).to include("NO FILE") + expect(output).not_to include("πŸ‘»") + end + end end diff --git a/spec/railbow_spec.rb b/spec/railbow_spec.rb new file mode 100644 index 0000000..1b40951 --- /dev/null +++ b/spec/railbow_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Railbow do + describe ".plain?" do + env_keys = %w[RBW_PLAIN RBW_FORCE NO_COLOR CLAUDECODE CI] + + around do |example| + saved = env_keys.to_h { |k| [k, ENV[k]] } + env_keys.each { |k| ENV.delete(k) } + example.run + ensure + saved.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v } + end + + before { allow($stdout).to receive(:tty?).and_return(true) } + + it "is false on a tty with no env overrides" do + expect(described_class.plain?).to be(false) + end + + it "is true when output is not a tty" do + allow($stdout).to receive(:tty?).and_return(false) + expect(described_class.plain?).to be(true) + end + + %w[NO_COLOR CLAUDECODE CI].each do |key| + it "is true when #{key} is set" do + ENV[key] = "1" + expect(described_class.plain?).to be(true) + end + + it "is false when #{key} is set but RBW_FORCE=1" do + ENV[key] = "1" + ENV["RBW_FORCE"] = "1" + expect(described_class.plain?).to be(false) + end + end + + it "is false with RBW_FORCE=1 when output is not a tty" do + allow($stdout).to receive(:tty?).and_return(false) + ENV["RBW_FORCE"] = "1" + expect(described_class.plain?).to be(false) + end + + it "is true when RBW_PLAIN=1, even with RBW_FORCE=1" do + ENV["RBW_PLAIN"] = "1" + ENV["RBW_FORCE"] = "1" + expect(described_class.plain?).to be(true) + end + + it "treats RBW_PLAIN=0 as unset and falls through to heuristics" do + ENV["RBW_PLAIN"] = "0" + ENV["CLAUDECODE"] = "1" + expect(described_class.plain?).to be(true) + end + + it "treats RBW_FORCE=0 as unset" do + ENV["RBW_FORCE"] = "0" + ENV["CI"] = "1" + expect(described_class.plain?).to be(true) + end + end +end From b16a73a9bfe591a2a5dc25e948ab669f53e433ab Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 23 Jul 2026 13:38:07 +0300 Subject: [PATCH 2/2] Prepare v0.3.0 --- CHANGELOG.md | 2 +- lib/railbow/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a5af7..d0fe598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.3.0] - 2026-07-23 ### Added diff --git a/lib/railbow/version.rb b/lib/railbow/version.rb index 6958a79..1e84f17 100644 --- a/lib/railbow/version.rb +++ b/lib/railbow/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Railbow - VERSION = "0.2.0" + VERSION = "0.3.0" end