Module: Browserctl::Commands::Migrate

Defined in:
lib/browserctl/commands/migrate.rb

Overview

‘browserctl migrate <path> [–to-version N] [–dry-run]` — operator entry point for the Migrations registry. Detects the artifact’s format and version, plans a chain of registered upgraders, and applies them in order (unless ‘–dry-run`).

The registry ships empty in v0.12; this command exists so operators have a stable invocation the moment a real migration lands. On an already-current artifact the command is a no-op and exits 0.

Constant Summary collapse

USAGE =
"Usage: browserctl migrate <path> [--to-version N] [--dry-run]"

Class Method Summary collapse

Class Method Details

.execute(path, target_version:, dry_run:, out:, err:) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/browserctl/commands/migrate.rb', line 45

def self.execute(path, target_version:, dry_run:, out:, err:)
  format = Browserctl::Migrations.detect_format(path)
  unless format
    err.puts "Error: could not detect format for #{path} (expected .bctl, .jsonl, or .rb)"
    exit Browserctl::Error::ExitCodes::PROTOCOL_MISMATCH
  end

  current = Browserctl::Migrations.detect_version(path, format)
  out.puts "Detected: format=#{format} version=#{current.inspect} path=#{path}"

  if dry_run
    plan_dry_run(format, current, target_version, out)
    return
  end

  result = Browserctl::Migrations.run(path, target_version: target_version)
  if result.applied.empty?
    out.puts "No migrations registered for #{format} v#{current}; nothing to do."
  else
    out.puts "Applied #{result.applied.size} migration(s): #{result.from} -> #{result.to}"
    result.applied.each { |m| out.puts "  - #{format} v#{m.from_version} -> v#{m.to_version}" }
  end
end

.latest_target(format, current) ⇒ Object



84
85
86
87
# File 'lib/browserctl/commands/migrate.rb', line 84

def self.latest_target(format, current)
  targets = registered_for(format)
  targets.empty? ? current : targets.max
end

.plan_dry_run(format, current, target_version, out) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/browserctl/commands/migrate.rb', line 69

def self.plan_dry_run(format, current, target_version, out)
  target = target_version || latest_target(format, current)
  chain  = Browserctl::Migrations.find_path(format: format, from: current, to: target)

  if chain.nil?
    out.puts "No migration path #{format} v#{current} -> v#{target} (registered: " \
             "#{registered_for(format).inspect})"
  elsif chain.empty?
    out.puts "Already at v#{target}; no migrations would run."
  else
    out.puts "Plan (#{chain.size} step(s)):"
    chain.each { |m| out.puts "  - #{format} v#{m.from_version} -> v#{m.to_version}" }
  end
end

.registered_for(format) ⇒ Object



89
90
91
# File 'lib/browserctl/commands/migrate.rb', line 89

def self.registered_for(format)
  Browserctl::Migrations.all.select { |m| m.format == format }.map(&:to_version)
end

.run(args, out: $stdout, err: $stderr) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/browserctl/commands/migrate.rb', line 21

def self.run(args, out: $stdout, err: $stderr)
  abort USAGE if args.empty? || args.include?("-h") || args.include?("--help")
  args = args.dup

  dry_run    = !args.delete("--dry-run").nil?
  target_idx = args.index("--to-version")
  target = if target_idx
             args.delete_at(target_idx)
             Integer(args.delete_at(target_idx))
           end
  path = args.shift
  abort USAGE unless path

  unless File.exist?(path)
    err.puts "Error: file not found: #{path}"
    exit Browserctl::Error::ExitCodes::GENERIC
  end

  execute(path, target_version: target, dry_run: dry_run, out: out, err: err)
rescue Browserctl::ProtocolMismatch => e
  err.puts "Error: #{e.message}"
  exit Browserctl::Error::ExitCodes.for(e.code)
end