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` — or `nil` when the format cannot be identified.
-
.detect_version(path, format) ⇒ Object
Reads ‘path` and returns the integer format_version it declares, or `nil` when no version header is present.
-
.find_path(format:, from:, to:) ⇒ Object
Breadth-first search through registered migrations to chain a path for ‘format` from `from` to `to`.
-
.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.
54 55 56 |
# File 'lib/browserctl/migrations.rb', line 54 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.
94 95 96 |
# File 'lib/browserctl/migrations.rb', line 94 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.
103 104 105 106 107 108 109 |
# File 'lib/browserctl/migrations.rb', line 103 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).
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/browserctl/migrations.rb', line 67 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 |
# File 'lib/browserctl/migrations.rb', line 44 def register(format:, from_version:, to_version:, &upgrade) raise ArgumentError, "upgrade block required" unless upgrade @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.
59 60 61 |
# File 'lib/browserctl/migrations.rb', line 59 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.
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/browserctl/migrations.rb', line 121 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 |