Module: Browserctl::Migrations
- Defined in:
- lib/browserctl/migrations.rb
Overview
Migration registry for browserctl persisted formats. Operators run
browserctl migrate <path> to upgrade an artifact written by an older
browserctl to the current build's format version.
Distinct from verify_format_version! (in bundle/recording/workflow):
those gates stay strict — a reader that encounters an unknown version
raises ProtocolMismatch. This module is the only blessed
path to mutate an old artifact in place.
The registry ships empty in v0.12. The first real migration arrives
only when a format actually changes post-1.0. See
docs/reference/format-versions.md ("Migration registry").
Defined Under Namespace
Constant Summary collapse
- FORMAT_EXTENSIONS =
{ ".bctl" => :bundle, ".jsonl" => :recording, ".rb" => :workflow }.freeze
Class Method Summary collapse
-
.all ⇒ Object
All registered migrations, in registration order.
-
.detect_format(path) ⇒ Object
Inspects an artifact path and returns a format symbol —
:bundle,:recording, or:workflow— ornilwhen the format cannot be identified. -
.detect_version(path, format) ⇒ Object
Reads
pathand returns the integer format_version it declares, ornilwhen no version header is present. -
.find_path(format:, from:, to:) ⇒ Object
Breadth-first search through registered migrations to chain a path for
formatfromfromtoto. -
.register(format:, from_version:, to_version:, &upgrade) ⇒ Object
Registers an upgrader for one hop in
format's version chain. -
.reset! ⇒ Object
Test-only hook — clears the registry.
-
.run(path, target_version: nil) ⇒ Object
End-to-end migration.
Class Method Details
.all ⇒ Object
All registered migrations, in registration order.
61 62 63 |
# File 'lib/browserctl/migrations.rb', line 61 def all @mutex.synchronize { @registry.dup } end |
.detect_format(path) ⇒ Object
Inspects an artifact path and returns a format symbol — :bundle,
:recording, or :workflow — or nil when the format cannot be
identified. Detection is extension-driven: keep new formats listed
in FORMAT_EXTENSIONS so this stays a one-line lookup.
101 102 103 |
# File 'lib/browserctl/migrations.rb', line 101 def detect_format(path) FORMAT_EXTENSIONS[File.extname(path.to_s).downcase] end |
.detect_version(path, format) ⇒ Object
Reads path and returns the integer format_version it declares, or
nil when no version header is present. Format-specific because
each format stores the header differently — the bundle in its
binary manifest, the recording in a _meta JSONL line, the
workflow in a Ruby comment.
110 111 112 113 114 115 116 |
# File 'lib/browserctl/migrations.rb', line 110 def detect_version(path, format) case format when :bundle then peek_bundle_version(path) when :recording then peek_recording_version(path) when :workflow then peek_workflow_version(path) end end |
.find_path(format:, from:, to:) ⇒ Object
Breadth-first search through registered migrations to chain a path
for format from from to to. Returns the ordered list of
Migration hops, or nil if no path is reachable. When from == to
returns an empty array (already at target — no work to do).
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/browserctl/migrations.rb', line 74 def find_path(format:, from:, to:) return [] if from == to all_for_format = all.select { |m| m.format == format } queue = [[from, []]] seen = { from => true } until queue.empty? current, path = queue.shift all_for_format.each do |m| next unless m.from_version == current next if seen[m.to_version] new_path = path + [m] return new_path if m.to_version == to seen[m.to_version] = true queue << [m.to_version, new_path] end end nil end |
.register(format:, from_version:, to_version:, &upgrade) ⇒ Object
Registers an upgrader for one hop in format's version chain. The
block receives the file path as a keyword argument and must rewrite
the file in place to the new version.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/browserctl/migrations.rb', line 44 def register(format:, from_version:, to_version:, &upgrade) unless upgrade raise Browserctl::Error.new( "upgrade block required", code: Browserctl::Error::Codes::INVALID_DSL_USAGE, context: { dsl: :migrations, action: :register, format: format, from_version: from_version, to_version: to_version } ) end @mutex.synchronize do @registry << Migration.new(format: format, from_version: from_version, to_version: to_version, upgrade: upgrade) end end |
.reset! ⇒ Object
Test-only hook — clears the registry. Not part of the public API.
66 67 68 |
# File 'lib/browserctl/migrations.rb', line 66 def reset! @mutex.synchronize { @registry.clear } end |
.run(path, target_version: nil) ⇒ Object
End-to-end migration. Detects format and current version, finds a
chain of registered upgraders to target_version (or the latest
to_version seen for this format if target_version is nil), and
invokes each in order. Each upgrader rewrites the file in place.
Returns a Result. When no migrations are needed (or the registry
has no entries for this format), applied is empty.
Raises ProtocolMismatch when format detection fails, the version cannot be read, or no chain reaches the target.
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/browserctl/migrations.rb', line 128 def run(path, target_version: nil) format = detect_format(path) or raise_protocol("could not detect format for #{path}") current = detect_version(path, format) or raise_protocol("could not read format_version from #{path}") target = target_version || latest_known_target(format, current) if current == target # If we'd be a no-op but the artifact's declared version is one this # build's reader does not support, surface that as PROTOCOL_MISMATCH — # there is no migration registered to bring it into range. unless format_version_supported?(format, current) raise_protocol("#{format} at #{path} declares unsupported format_version=#{current}; " \ "no migration registered") end return Result.new(format: format, from: current, to: current, applied: []) end chain = find_path(format: format, from: current, to: target) raise_protocol("no migration path from #{format} v#{current} to v#{target}") if chain.nil? chain.each { |m| m.upgrade.call(path: path, from_version: m.from_version, to_version: m.to_version) } Result.new(format: format, from: current, to: target, applied: chain) end |