Module: Textus::Publisher

Defined in:
lib/textus/publisher.rb

Overview

Publishes built artifacts from the store to repo-relative consumer paths. Publish = copy + sentinel. The in-store file is already the consumer-shaped artifact; no parsing or stripping. Sentinels live under ‘<store_root>/sentinels/` and mirror the target’s repo-relative layout so consumer directories aren’t polluted with ‘.textus-managed.json` siblings.

Constant Summary collapse

SENTINEL_SUFFIX =
".textus-managed.json".freeze
SENTINEL_DIR =
"sentinels".freeze

Class Method Summary collapse

Class Method Details

.cleanup_legacy_sentinel(target) ⇒ Object



59
60
61
# File 'lib/textus/publisher.rb', line 59

def self.cleanup_legacy_sentinel(target)
  FileUtils.rm_f(legacy_sentinel_path(target))
end

.legacy_sentinel_path(target) ⇒ Object



55
56
57
# File 'lib/textus/publisher.rb', line 55

def self.legacy_sentinel_path(target)
  target + SENTINEL_SUFFIX
end

.managed?(target, store_root) ⇒ Boolean

Returns:

  • (Boolean)


31
32
33
# File 'lib/textus/publisher.rb', line 31

def self.managed?(target, store_root)
  File.exist?(sentinel_path(target, store_root)) || File.exist?(legacy_sentinel_path(target))
end

.publish(source:, target:, store_root:) ⇒ Object



15
16
17
18
19
20
21
22
# File 'lib/textus/publisher.rb', line 15

def self.publish(source:, target:, store_root:)
  FileUtils.mkdir_p(File.dirname(target))
  refuse_if_unmanaged(target, store_root)
  File.delete(target) if File.symlink?(target)
  FileUtils.cp(source, target)
  write_sentinel(target, store_root: store_root, source: source)
  cleanup_legacy_sentinel(target)
end

.refuse_if_unmanaged(target, store_root) ⇒ Object

Raises:



24
25
26
27
28
29
# File 'lib/textus/publisher.rb', line 24

def self.refuse_if_unmanaged(target, store_root)
  return unless File.exist?(target) || File.symlink?(target)
  return if managed?(target, store_root)

  raise PublishError.new("refusing to clobber unmanaged file at #{target}", target: target)
end

.relative_to(path, base) ⇒ Object



63
64
65
66
67
68
69
# File 'lib/textus/publisher.rb', line 63

def self.relative_to(path, base)
  path = File.expand_path(path)
  base = File.expand_path(base)
  return nil unless path.start_with?(base + File::SEPARATOR)

  path[(base.length + 1)..]
end

.sentinel_path(target, store_root) ⇒ Object

Sentinel layout: <store_root>/sentinels/<target_rel_to_repo>.textus-managed.json The full target extension is preserved so a marketplace.json and marketplace.yaml don’t collide.



49
50
51
52
53
# File 'lib/textus/publisher.rb', line 49

def self.sentinel_path(target, store_root)
  repo_root = File.dirname(store_root)
  rel = relative_to(target, repo_root) || File.basename(target)
  File.join(store_root, SENTINEL_DIR, rel + SENTINEL_SUFFIX)
end

.write_sentinel(target, store_root:, source:) ⇒ Object



35
36
37
38
39
40
41
42
43
44
# File 'lib/textus/publisher.rb', line 35

def self.write_sentinel(target, store_root:, source:)
  path = sentinel_path(target, store_root)
  FileUtils.mkdir_p(File.dirname(path))
  File.write(path, JSON.generate(
                     "source" => source,
                     "target" => target,
                     "sha256" => Digest::SHA256.hexdigest(File.binread(target)),
                     "mode" => "copy",
                   ))
end