metanorma-release 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +19 -1
  3. data/.rubocop_todo.yml +250 -319
  4. data/README.adoc +120 -233
  5. data/Rakefile +2 -2
  6. data/exe/metanorma-release +2 -2
  7. data/lib/metanorma/release/aggregation_pipeline.rb +59 -45
  8. data/lib/metanorma/release/asset_processor.rb +10 -8
  9. data/lib/metanorma/release/cache_store.rb +6 -6
  10. data/lib/metanorma/release/change_detector.rb +7 -3
  11. data/lib/metanorma/release/channel.rb +13 -39
  12. data/lib/metanorma/release/channel_filter.rb +26 -10
  13. data/lib/metanorma/release/cli.rb +129 -100
  14. data/lib/metanorma/release/commands/aggregate.rb +39 -54
  15. data/lib/metanorma/release/commands/package.rb +20 -12
  16. data/lib/metanorma/release/commands/{publish.rb → release_command.rb} +20 -12
  17. data/lib/metanorma/release/config.rb +104 -0
  18. data/lib/metanorma/release/content_hash.rb +11 -3
  19. data/lib/metanorma/release/delta_state.rb +55 -18
  20. data/lib/metanorma/release/file_routing.rb +8 -5
  21. data/lib/metanorma/release/index.rb +132 -0
  22. data/lib/metanorma/release/interfaces.rb +15 -15
  23. data/lib/metanorma/release/platform/github/manifest_reader.rb +4 -4
  24. data/lib/metanorma/release/platform/github/publisher.rb +23 -11
  25. data/lib/metanorma/release/platform/github/release_fetcher.rb +12 -3
  26. data/lib/metanorma/release/platform/github.rb +10 -7
  27. data/lib/metanorma/release/platform/local/directory_discoverer.rb +1 -1
  28. data/lib/metanorma/release/platform/local/fetcher.rb +17 -12
  29. data/lib/metanorma/release/platform/local/publisher.rb +9 -7
  30. data/lib/metanorma/release/platform/local.rb +4 -4
  31. data/lib/metanorma/release/platform/null/publisher.rb +3 -2
  32. data/lib/metanorma/release/platform/null.rb +1 -1
  33. data/lib/metanorma/release/platform.rb +3 -3
  34. data/lib/metanorma/release/platform_factory.rb +48 -29
  35. data/lib/metanorma/release/publication.rb +335 -0
  36. data/lib/metanorma/release/release_pipeline.rb +85 -52
  37. data/lib/metanorma/release/repo_ref.rb +5 -2
  38. data/lib/metanorma/release/site.rb +66 -0
  39. data/lib/metanorma/release/slug_strategy.rb +163 -0
  40. data/lib/metanorma/release/version.rb +1 -1
  41. data/lib/metanorma/release/zip_packager.rb +31 -8
  42. data/lib/metanorma/release.rb +68 -94
  43. metadata +22 -26
  44. data/lib/metanorma/release/aggregation_interfaces.rb +0 -27
  45. data/lib/metanorma/release/channel_audience.rb +0 -24
  46. data/lib/metanorma/release/channel_config.rb +0 -55
  47. data/lib/metanorma/release/channel_manifest.rb +0 -192
  48. data/lib/metanorma/release/channel_registry.rb +0 -60
  49. data/lib/metanorma/release/config_fetcher.rb +0 -11
  50. data/lib/metanorma/release/config_locator.rb +0 -37
  51. data/lib/metanorma/release/config_resolver.rb +0 -37
  52. data/lib/metanorma/release/document_id.rb +0 -45
  53. data/lib/metanorma/release/document_index.rb +0 -183
  54. data/lib/metanorma/release/document_metadata.rb +0 -39
  55. data/lib/metanorma/release/document_stage.rb +0 -86
  56. data/lib/metanorma/release/document_type.rb +0 -55
  57. data/lib/metanorma/release/document_version.rb +0 -50
  58. data/lib/metanorma/release/naming_strategy.rb +0 -158
  59. data/lib/metanorma/release/platform/github/config_fetcher.rb +0 -40
  60. data/lib/metanorma/release/platform/local/config_fetcher.rb +0 -20
  61. data/lib/metanorma/release/rake_tasks.rb +0 -71
  62. data/lib/metanorma/release/relaton_enricher.rb +0 -138
  63. data/lib/metanorma/release/release_metadata.rb +0 -79
  64. data/lib/metanorma/release/release_tag.rb +0 -49
  65. data/lib/metanorma/release/rxl_extractor.rb +0 -115
  66. data/lib/metanorma/release/stage_filter.rb +0 -18
@@ -7,8 +7,9 @@ module Metanorma
7
7
  class Publisher
8
8
  include Metanorma::Release::Publisher
9
9
 
10
- def publish(tag, _artifact, _metadata, channels:, force_replace: false)
11
- PublishResult.new(tag: tag.to_s, url: 'null://', created?: true)
10
+ def publish(tag, _artifact, _metadata, channels:,
11
+ force_replace: false)
12
+ PublishResult.new(tag: tag.to_s, url: "null://", created?: true)
12
13
  end
13
14
  end
14
15
  end
@@ -4,7 +4,7 @@ module Metanorma
4
4
  module Release
5
5
  module Platform
6
6
  module Null
7
- autoload :Publisher, 'metanorma/release/platform/null/publisher'
7
+ autoload :Publisher, "metanorma/release/platform/null/publisher"
8
8
  end
9
9
  end
10
10
  end
@@ -3,9 +3,9 @@
3
3
  module Metanorma
4
4
  module Release
5
5
  module Platform
6
- autoload :GitHub, 'metanorma/release/platform/github'
7
- autoload :Local, 'metanorma/release/platform/local'
8
- autoload :Null, 'metanorma/release/platform/null'
6
+ autoload :GitHub, "metanorma/release/platform/github"
7
+ autoload :Local, "metanorma/release/platform/local"
8
+ autoload :Null, "metanorma/release/platform/null"
9
9
  end
10
10
  end
11
11
  end
@@ -4,59 +4,77 @@ module Metanorma
4
4
  module Release
5
5
  module PlatformFactory
6
6
  PUBLISHER_REGISTRY = {
7
- 'null' => ->(_opts) { Platform::Null::Publisher.new },
8
- 'local' => ->(opts) { Platform::Local::Publisher.new(output_dir: opts[:output_dir]) }
9
- }.freeze
7
+ "null" => ->(_opts) { Platform::Null::Publisher.new },
8
+ "local" => ->(opts) {
9
+ Platform::Local::Publisher.new(output_dir: opts[:output_dir])
10
+ },
11
+ }.dup
10
12
 
11
13
  AGGREGATION_REGISTRY = {
12
- 'local' => lambda { |opts, _token|
13
- path = opts[:source].sub('local:', '')
14
+ "local" => lambda { |opts, _token|
15
+ path = opts[:source].sub("local:", "")
14
16
  {
15
17
  discoverer: Platform::Local::DirectoryDiscoverer.new(base_path: path),
16
- fetcher: Platform::Local::Fetcher.new(base_path: path)
18
+ fetcher: Platform::Local::Fetcher.new(base_path: path),
17
19
  }
18
- }
19
- }.freeze
20
+ },
21
+ "github" => lambda { |opts, token|
22
+ require "octokit"
23
+ client = build_github_client(token)
24
+
25
+ discoverer = if opts[:repos]
26
+ repos = opts[:repos].map { |r| RepoRef.from_string(r) }
27
+ StaticDiscoverer.new(repos: repos)
28
+ else
29
+ Platform::GitHub::TopicDiscoverer.new(
30
+ client: client, organizations: opts[:organizations], topic: opts[:topic],
31
+ )
32
+ end
33
+
34
+ {
35
+ discoverer: discoverer,
36
+ fetcher: Platform::GitHub::ReleaseFetcher.new(client: client),
37
+ manifest_reader: Platform::GitHub::ManifestReader.new(client: client),
38
+ }
39
+ },
40
+ }.dup
20
41
 
21
42
  def self.build_publisher(platform, options)
22
43
  factory = PUBLISHER_REGISTRY[platform]
23
- raise ArgumentError, "Unknown platform: #{platform}. Available: #{PUBLISHER_REGISTRY.keys.join(', ')}" unless factory
44
+ unless factory
45
+ raise ArgumentError,
46
+ "Unknown platform: #{platform}. Available: #{PUBLISHER_REGISTRY.keys.join(', ')}"
47
+ end
24
48
 
25
49
  factory.call(options)
26
50
  end
27
51
 
28
52
  def self.build_aggregation_adapters(options)
29
53
  source = options[:source]
30
- if source.start_with?('local:')
31
- adapters = AGGREGATION_REGISTRY['local'].call(options, options[:token])
54
+
55
+ if source.start_with?("local:")
56
+ adapters = AGGREGATION_REGISTRY["local"].call(options,
57
+ options[:token])
32
58
  adapters[:manifest_reader] = NullManifestReader.new
33
59
  return adapters
34
60
  end
35
61
 
36
- require 'octokit'
37
- client = build_github_client(options[:token])
38
-
39
- discoverer = if options[:repos]
40
- repos = options[:repos].map { |r| RepoRef.from_string(r) }
41
- StaticDiscoverer.new(repos: repos)
42
- else
43
- Platform::GitHub::TopicDiscoverer.new(
44
- client: client, organizations: options[:organizations], topic: options[:topic]
45
- )
46
- end
47
-
48
- {
49
- discoverer: discoverer,
50
- fetcher: Platform::GitHub::ReleaseFetcher.new(client: client),
51
- manifest_reader: Platform::GitHub::ManifestReader.new(client: client)
52
- }
62
+ AGGREGATION_REGISTRY["github"].call(options, options[:token])
53
63
  end
54
64
 
55
65
  def self.build_github_client(token)
56
- require 'octokit'
66
+ require "octokit"
57
67
  token ? Octokit::Client.new(access_token: token) : Octokit::Client.new
58
68
  end
59
69
 
70
+ def self.register_publisher(name, factory)
71
+ PUBLISHER_REGISTRY[name] = factory
72
+ end
73
+
74
+ def self.register_aggregation(name, factory)
75
+ AGGREGATION_REGISTRY[name] = factory
76
+ end
77
+
60
78
  class StaticDiscoverer
61
79
  include RepoDiscoverer
62
80
 
@@ -71,6 +89,7 @@ module Metanorma
71
89
 
72
90
  class NullManifestReader
73
91
  include Metanorma::Release::ManifestReader
92
+
74
93
  def read(_repo) = nil
75
94
  end
76
95
  end
@@ -0,0 +1,335 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "relaton/bib"
5
+ rescue LoadError
6
+ raise LoadError,
7
+ "The relaton-bib gem is required. Add `gem 'relaton-bib'` to your Gemfile."
8
+ end
9
+
10
+ require "json"
11
+
12
+ module Metanorma
13
+ module Release
14
+ class PublicationFile
15
+ attr_reader :format, :name, :path
16
+
17
+ def initialize(format:, name:, path:)
18
+ @format = format
19
+ @name = name
20
+ @path = path
21
+ freeze
22
+ end
23
+
24
+ def eql?(other)
25
+ other.is_a?(self.class) && format == other.format && path == other.path
26
+ end
27
+
28
+ def hash
29
+ [format, path].hash
30
+ end
31
+
32
+ def to_h
33
+ { "format" => format, "name" => name, "path" => path }
34
+ end
35
+ end
36
+
37
+ class PublicationSource
38
+ attr_reader :owner, :repo, :tag, :url, :date
39
+
40
+ def initialize(owner:, repo:, tag:, url:, date:)
41
+ @owner = owner
42
+ @repo = repo
43
+ @tag = tag
44
+ @url = url
45
+ @date = date
46
+ freeze
47
+ end
48
+
49
+ def repo_key
50
+ "#{owner}/#{repo}"
51
+ end
52
+
53
+ def eql?(other)
54
+ other.is_a?(self.class) && owner == other.owner && repo == other.repo && tag == other.tag
55
+ end
56
+
57
+ def hash
58
+ [owner, repo, tag].hash
59
+ end
60
+
61
+ def to_h
62
+ { "owner" => owner, "repo" => repo, "tag" => tag,
63
+ "releaseUrl" => url, "releaseDate" => date }
64
+ end
65
+ end
66
+
67
+ class Publication
68
+ METADATA_VERSION = 1
69
+
70
+ attr_reader :identifier, :slug, :title, :edition, :stage, :doctype,
71
+ :revdate, :files, :channels, :source
72
+
73
+ def initialize(identifier:, slug:, title:, edition:, stage:, doctype:,
74
+ revdate:, files:, channels:, source: nil, metadata_formats: nil)
75
+ @identifier = identifier
76
+ @slug = slug
77
+ @title = title
78
+ @edition = edition
79
+ @stage = stage
80
+ @doctype = doctype
81
+ @revdate = revdate
82
+ @files = files.freeze
83
+ @channels = channels.freeze
84
+ @source = source
85
+ @metadata_formats = metadata_formats
86
+ freeze
87
+ end
88
+
89
+ def formats
90
+ @metadata_formats || files.map(&:format)
91
+ end
92
+
93
+ def base_dir
94
+ files.any? ? File.dirname(files.first.path) : "."
95
+ end
96
+
97
+ def content_hash
98
+ ContentHash.of_directory(base_dir, base: slug)
99
+ end
100
+
101
+ def file?(format)
102
+ formats.include?(format)
103
+ end
104
+
105
+ def eql?(other)
106
+ other.is_a?(self.class) && identifier == other.identifier &&
107
+ edition == other.edition && stage == other.stage
108
+ end
109
+
110
+ def hash
111
+ [identifier, edition, stage].hash
112
+ end
113
+
114
+ def to_h
115
+ h = {
116
+ "id" => slug, "identifier" => identifier, "title" => title,
117
+ "edition" => edition, "stage" => stage, "doctype" => doctype,
118
+ "channels" => channels, "formats" => formats,
119
+ "revdate" => revdate
120
+ }
121
+ h["source"] = source.to_h if source
122
+ h["files"] = files.map(&:to_h) unless files.empty?
123
+ h
124
+ end
125
+
126
+ def with_channels(new_channels)
127
+ self.class.new(
128
+ identifier: identifier, slug: slug, title: title,
129
+ edition: edition, stage: stage, doctype: doctype,
130
+ revdate: revdate, files: files,
131
+ channels: new_channels, source: source,
132
+ metadata_formats: @metadata_formats
133
+ )
134
+ end
135
+
136
+ def with_files_and_source(new_files, release, repo)
137
+ source = PublicationSource.new(
138
+ owner: repo.owner, repo: repo.repo,
139
+ tag: release.tag_name,
140
+ url: release.html_url,
141
+ date: release.published_at
142
+ )
143
+ pub_files = new_files.map do |f|
144
+ name = File.basename(f)
145
+ ext = File.extname(name).delete_prefix(".")
146
+ PublicationFile.new(format: ext, name: name, path: f)
147
+ end
148
+ self.class.new(
149
+ identifier: identifier, slug: slug, title: title,
150
+ edition: edition, stage: stage, doctype: doctype,
151
+ revdate: revdate, files: pub_files,
152
+ channels: channels, source: source
153
+ )
154
+ end
155
+
156
+ def to_release_body
157
+ "<!-- mn-release-metadata\n#{JSON.generate(metadata_hash)}\n-->"
158
+ end
159
+
160
+ def to_json(*_args)
161
+ JSON.generate(metadata_hash)
162
+ end
163
+
164
+ def self.from_json(json_string)
165
+ data = JSON.parse(json_string)
166
+ raise ArgumentError, "Missing required field: id" unless data["id"]
167
+ unless data["title"]
168
+ raise ArgumentError,
169
+ "Missing required field: title"
170
+ end
171
+
172
+ from_metadata_hash(data)
173
+ end
174
+
175
+ def self.from_release_body(body)
176
+ return nil if body.nil? || body.empty?
177
+
178
+ match = body.match(/<!--\s*mn-release-metadata\s*\n(.*?)\n-->/m)
179
+ return nil unless match
180
+
181
+ from_json(match[1])
182
+ rescue JSON::ParserError
183
+ nil
184
+ end
185
+
186
+ def self.from_metadata_hash(data)
187
+ ident = data["identifier"] || data["id"]
188
+ new(
189
+ identifier: ident,
190
+ slug: slug_from_identifier(ident),
191
+ title: data["title"],
192
+ edition: data["edition"],
193
+ stage: data["stage"],
194
+ doctype: data["doctype"],
195
+ revdate: data["revdate"],
196
+ files: [],
197
+ channels: data["channels"] || [],
198
+ source: nil,
199
+ metadata_formats: data["formats"],
200
+ )
201
+ end
202
+
203
+ private
204
+
205
+ def metadata_hash
206
+ {
207
+ "version" => METADATA_VERSION,
208
+ "id" => slug,
209
+ "identifier" => identifier,
210
+ "title" => title,
211
+ "edition" => edition,
212
+ "stage" => stage,
213
+ "doctype" => doctype,
214
+ "revdate" => revdate,
215
+ "formats" => formats,
216
+ "channels" => channels,
217
+ "publisher" => Publication.publisher_from_identifier(identifier),
218
+ }
219
+ end
220
+
221
+ def self.slug_from_identifier(identifier)
222
+ identifier.to_s.strip
223
+ .gsub(/\s+/, "-")
224
+ .gsub(/:+/, "-")
225
+ .downcase
226
+ .gsub(/--+/, "-")
227
+ .gsub(/[-.]+$/, "")
228
+ end
229
+
230
+ def self.publisher_from_identifier(identifier)
231
+ return nil if identifier.nil? || identifier.strip.empty?
232
+
233
+ identifier.strip.split(/[\s-]/).first&.downcase
234
+ end
235
+
236
+ # -- RXL extraction --
237
+
238
+ def self.discover(output_dir)
239
+ Dir.glob(File.join(output_dir, "**", "*.rxl")).filter_map do |path|
240
+ from_rxl(path)
241
+ rescue StandardError => e
242
+ warn "Warning: Skipping #{path}: #{e.message}"
243
+ nil
244
+ end
245
+ end
246
+
247
+ def self.from_rxl(rxl_path)
248
+ unless File.exist?(rxl_path)
249
+ raise ArgumentError,
250
+ "RXL file not found: #{rxl_path}"
251
+ end
252
+
253
+ content = File.read(rxl_path)
254
+ bib = Relaton::Bib::Item.from_xml(content)
255
+ build_from_bib(bib, rxl_path)
256
+ rescue StandardError => e
257
+ warn "Warning: Failed to parse RXL #{rxl_path}: #{e.message}"
258
+ fallback_from_rxl(rxl_path)
259
+ end
260
+
261
+ class << self
262
+ private
263
+
264
+ def build_from_bib(bib, rxl_path)
265
+ identifier = bib.docidentifier&.first&.content || ""
266
+ slug = slug_from_identifier(identifier)
267
+ output_dir = File.dirname(rxl_path)
268
+ base_name = File.basename(rxl_path, ".rxl")
269
+
270
+ new(
271
+ identifier: identifier, slug: slug,
272
+ title: bib.title&.first&.content || "",
273
+ edition: extract_edition(bib),
274
+ stage: extract_stage(bib),
275
+ doctype: extract_doctype(bib),
276
+ revdate: extract_revdate(bib),
277
+ files: discover_files(output_dir, base_name),
278
+ channels: [], source: nil
279
+ )
280
+ end
281
+
282
+ def extract_edition(bib)
283
+ ed = bib.edition
284
+ return "1" unless ed
285
+
286
+ ed.respond_to?(:content) ? ed.content.to_s : ed.to_s
287
+ end
288
+
289
+ def extract_stage(bib)
290
+ stage = bib.status&.stage
291
+ return "" unless stage
292
+
293
+ stage.respond_to?(:content) ? stage.content.to_s : stage.to_s
294
+ end
295
+
296
+ def extract_doctype(bib)
297
+ doctype = bib.ext&.doctype
298
+ return "" unless doctype
299
+
300
+ doctype.respond_to?(:content) ? doctype.content.to_s : doctype.to_s
301
+ end
302
+
303
+ def extract_revdate(bib)
304
+ date = bib.date&.find { |d| d.type == "published" } || bib.date&.first
305
+ return nil unless date
306
+
307
+ on = date.on
308
+ on.respond_to?(:content) ? on.content.to_s : on.to_s
309
+ rescue StandardError
310
+ nil
311
+ end
312
+
313
+ def discover_files(output_dir, base_name)
314
+ Dir.glob(File.join(output_dir, "#{base_name}.*")).filter_map do |path|
315
+ next if File.directory?(path)
316
+
317
+ name = File.basename(path)
318
+ ext = File.extname(name).delete_prefix(".")
319
+ PublicationFile.new(format: ext, name: name, path: name)
320
+ end
321
+ end
322
+
323
+ def fallback_from_rxl(rxl_path)
324
+ base_name = File.basename(rxl_path, ".rxl")
325
+ slug = slug_from_identifier(base_name)
326
+ new(
327
+ identifier: base_name, slug: slug, title: "",
328
+ edition: "0", stage: "", doctype: "",
329
+ revdate: nil, files: [], channels: [], source: nil
330
+ )
331
+ end
332
+ end
333
+ end
334
+ end
335
+ end
@@ -2,13 +2,48 @@
2
2
 
3
3
  module Metanorma
4
4
  module Release
5
+ PublishResult = Struct.new(:tag, :url, :created?, keyword_init: true)
6
+ ReleasedArtifact = Struct.new(:id, :tag, :url, :channels,
7
+ keyword_init: true)
8
+ ReleaseResult = Struct.new(:released, :skipped, :failed,
9
+ :released_artifacts, keyword_init: true)
10
+
5
11
  class ReleasePipeline
6
12
  Dependencies = Struct.new(
7
13
  :extractor, :filters, :change_detector,
8
- :packager, :publisher, :naming_registry,
9
- :manifest, :channel_override, :channel_config,
14
+ :packager, :publisher, :slug_registry,
15
+ :manifest, :channel_override, :config,
10
16
  keyword_init: true
11
- )
17
+ ) do
18
+ def initialize(**kwargs)
19
+ super
20
+ validate_types!
21
+ end
22
+
23
+ private
24
+
25
+ def validate_types!
26
+ unless extractor.respond_to?(:discover)
27
+ raise ArgumentError,
28
+ "extractor must respond to #discover, got #{extractor.class}"
29
+ end
30
+
31
+ validate_interface!(change_detector, ChangeDetector,
32
+ "change_detector")
33
+ validate_interface!(packager, Packager, "packager")
34
+ validate_interface!(publisher, Publisher, "publisher")
35
+ end
36
+
37
+ def validate_interface!(obj, mod, name)
38
+ return if obj.is_a?(mod) || begin
39
+ obj.class.ancestors.include?(mod)
40
+ rescue StandardError
41
+ false
42
+ end
43
+
44
+ raise ArgumentError, "#{name} must include #{mod}, got #{obj.class}"
45
+ end
46
+ end
12
47
 
13
48
  Config = Struct.new(
14
49
  :output_dir, :manifest_path, :force, :force_replace_patterns,
@@ -21,29 +56,31 @@ module Metanorma
21
56
  end
22
57
 
23
58
  def run(config)
24
- documents = @deps.extractor.discover(config.output_dir)
25
- filtered = apply_filters(documents)
59
+ publications = @deps.extractor.discover(config.output_dir)
60
+ filtered = apply_filters(publications)
26
61
  results = phase_one(filtered, config)
27
62
  phase_two(results, config)
28
63
  end
29
64
 
30
65
  private
31
66
 
32
- def apply_filters(documents)
33
- return documents unless @deps.filters && !@deps.filters.empty?
67
+ def apply_filters(publications)
68
+ return publications unless @deps.filters && !@deps.filters.empty?
34
69
 
35
- @deps.filters.reduce(documents) { |docs, filter| filter.apply(docs) }
70
+ @deps.filters.reduce(publications) { |docs, filter| filter.apply(docs) }
36
71
  end
37
72
 
38
- def phase_one(documents, config)
39
- documents.map do |doc|
40
- strategy = @deps.naming_registry.resolve(doc.document_type)
41
- tag = strategy.compute_tag(doc.id.to_s, doc.version)
42
- canonical_base = strategy.compute_canonical_base(doc.id.to_s, doc.version)
43
- change = @deps.change_detector.detect(doc, tag, force: config.force)
44
-
45
- { document: doc, tag: tag, canonical_base: canonical_base,
46
- changed: change.changed?, change_result: change }
73
+ def phase_one(publications, config)
74
+ publications.map do |pub|
75
+ publisher = Publication.publisher_from_identifier(pub.identifier)
76
+ strategy = @deps.slug_registry.resolve(publisher)
77
+ tag_info = strategy.compute_tag(pub)
78
+ canonical_base = strategy.compute_asset_name(pub).sub(/\.zip$/, "")
79
+ change = @deps.change_detector.detect(pub, tag_info[:tag],
80
+ force: config.force)
81
+
82
+ { publication: pub, tag: tag_info[:tag], pre_release: tag_info[:pre_release],
83
+ canonical_base: canonical_base, changed: change.changed?, change_result: change }
47
84
  end
48
85
  end
49
86
 
@@ -54,34 +91,38 @@ module Metanorma
54
91
  released_artifacts = []
55
92
 
56
93
  candidates.each do |candidate|
57
- doc = candidate[:document]
94
+ pub = candidate[:publication]
58
95
  tag = candidate[:tag]
59
96
 
60
97
  unless candidate[:changed]
61
- skipped << doc
98
+ skipped << pub
62
99
  next
63
100
  end
64
101
 
65
102
  begin
66
- policy = resolve_policy(doc, config)
67
- unless policy.release?
68
- skipped << doc
103
+ channels = resolve_channels(pub)
104
+ if channels.empty?
105
+ skipped << pub
69
106
  next
70
107
  end
71
108
 
72
- artifact = @deps.packager.package(doc, canonical_base: candidate[:canonical_base])
73
- channels = resolve_channels(doc, policy)
74
- metadata_json = ReleaseMetadata.from_document(doc, channels: channels)
75
- force = config.force_replace_patterns&.any? { |p| File.fnmatch?(p, doc.id.to_s) } || false
76
-
77
- result = @deps.publisher.publish(tag, artifact, metadata_json, channels: channels, force_replace: force)
78
- released << doc
109
+ artifact = @deps.packager.package(pub,
110
+ canonical_base: candidate[:canonical_base])
111
+ channel_objects = channels.map { |c| Channel.new(c) }
112
+ pub_for_release = pub.with_channels(channels)
113
+ force = config.force_replace_patterns&.any? do |p|
114
+ File.fnmatch?(p, pub.slug)
115
+ end || false
116
+
117
+ result = @deps.publisher.publish(tag, artifact, pub_for_release,
118
+ channels: channel_objects, force_replace: force)
119
+ released << pub
79
120
  released_artifacts << ReleasedArtifact.new(
80
- id: doc.id.to_s, tag: tag.to_s,
81
- url: result.url, channels: channels.map(&:to_s)
121
+ id: pub.slug, tag: tag.to_s,
122
+ url: result.url, channels: channels
82
123
  )
83
124
  rescue StandardError => e
84
- failed << { document: doc, error: e.message }
125
+ failed << { document: pub, error: e.message }
85
126
  end
86
127
  end
87
128
 
@@ -89,26 +130,18 @@ module Metanorma
89
130
  released_artifacts: released_artifacts)
90
131
  end
91
132
 
92
- def resolve_policy(doc, _config)
93
- return @deps.manifest.resolve(doc) if @deps.manifest
94
-
95
- DocumentReleasePolicy.from_defaults('public', [Channel.public('default')])
96
- end
97
-
98
- def resolve_channels(_doc, policy)
99
- channels = if @deps.channel_override && !@deps.channel_override.empty?
100
- @deps.channel_override
101
- else
102
- policy.channels
103
- end
104
-
105
- validate_channels(channels)
106
- end
107
-
108
- def validate_channels(channels)
109
- return channels unless @deps.channel_config
110
-
111
- channels.select { |ch| @deps.channel_config.registry.valid?(ch) }
133
+ def resolve_channels(pub)
134
+ override = @deps.channel_override
135
+ return override if override && !override.empty?
136
+
137
+ if @deps.config
138
+ @deps.config.resolve_channels(pub)
139
+ elsif @deps.manifest
140
+ policy = @deps.manifest.resolve(pub)
141
+ policy&.channels&.map(&:to_s) || ["public"]
142
+ else
143
+ ["public"]
144
+ end
112
145
  end
113
146
  end
114
147
  end
@@ -6,8 +6,11 @@ module Metanorma
6
6
  attr_reader :owner, :repo
7
7
 
8
8
  def self.from_string(str)
9
- parts = str.split('/', 2)
10
- raise ArgumentError, "Invalid repo reference: #{str}" unless parts.length == 2
9
+ parts = str.split("/", 2)
10
+ unless parts.length == 2
11
+ raise ArgumentError,
12
+ "Invalid repo reference: #{str}"
13
+ end
11
14
 
12
15
  new(owner: parts[0], repo: parts[1])
13
16
  end