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
@@ -1,129 +1,158 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'optparse'
3
+ require "thor"
4
4
 
5
5
  module Metanorma
6
6
  module Release
7
- module CLI
8
- module_function
9
-
10
- def run(argv)
11
- command = argv.shift
12
- case command
13
- when 'package' then run_package(argv)
14
- when 'publish' then run_publish(argv)
15
- when 'aggregate' then run_aggregate(argv)
16
- when nil
17
- warn 'Usage: metanorma-release <package|publish|aggregate> [options]'
18
- exit 2
19
- else
20
- warn "Unknown command: #{command}"
21
- exit 2
22
- end
7
+ class CLI < Thor
8
+ def self.exit_on_failure?
9
+ true
23
10
  end
24
11
 
25
- def run_package(argv)
26
- config = parse_package_args(argv)
12
+ class PipelineError < Thor::Error; end
13
+
14
+ desc "package", "Package compiled documents"
15
+ option :output_dir, type: :string, default: "_site",
16
+ desc: "Compiled docs directory"
17
+ option :dest, type: :string, default: "dist",
18
+ desc: "Destination for packages"
19
+ option :manifest, type: :string, default: "metanorma.release.yml",
20
+ desc: "Release manifest file"
21
+ option :config, type: :string, desc: "Config file"
22
+
23
+ def package
24
+ config = PackageCommand::Config.new(
25
+ output_dir: options[:output_dir],
26
+ dest: options[:dest],
27
+ manifest: options[:manifest],
28
+ config_source: options[:config],
29
+ )
27
30
  result = PackageCommand.new(config).call
28
31
  print_package_result(result, config.dest)
29
- exit(result.failed.empty? ? 0 : 1)
32
+ raise PipelineError, format_failures(result) unless result.failed.empty?
30
33
  end
31
34
 
32
- def run_publish(argv)
33
- config = parse_publish_args(argv)
34
- result = PublishCommand.new(config).call
35
+ desc "release", "Package and release documents"
36
+ option :platform, type: :string, default: "github",
37
+ desc: "Publishing platform (github|local)"
38
+ option :output_dir, type: :string, default: "_site",
39
+ desc: "Compiled docs directory"
40
+ option :manifest, type: :string, default: "metanorma.release.yml",
41
+ desc: "Release manifest file"
42
+ option :force, type: :boolean, default: false,
43
+ desc: "Force release even if unchanged"
44
+ option :force_replace, type: :array, default: [],
45
+ desc: "Glob patterns for force-replace"
46
+ option :channels, type: :array, desc: "Override channels"
47
+ option :concurrency, type: :numeric, default: 4
48
+ option :token, type: :string, desc: "Platform auth token"
49
+ option :config, type: :string, desc: "Config file"
50
+
51
+ def release
52
+ config = ReleaseCommand::Config.new(
53
+ output_dir: options[:output_dir],
54
+ platform: options[:platform],
55
+ manifest: options[:manifest],
56
+ force: options[:force],
57
+ force_replace: options[:force_replace],
58
+ channels: options[:channels],
59
+ concurrency: options[:concurrency],
60
+ token: options[:token],
61
+ config_source: options[:config],
62
+ )
63
+ result = ReleaseCommand.new(config).call
35
64
  print_publish_result(result)
36
- exit(result.failed.empty? ? 0 : 1)
65
+ raise PipelineError, format_failures(result) unless result.failed.empty?
37
66
  end
38
67
 
39
- def run_aggregate(argv)
40
- config = parse_aggregate_args(argv)
68
+ desc "aggregate", "Aggregate released documents"
69
+ option :source, type: :string, default: "github",
70
+ desc: "Source (github|local:PATH)"
71
+ option :organizations, type: :array, default: [],
72
+ desc: "Organizations to discover"
73
+ option :topic, type: :string, default: "metanorma-release",
74
+ desc: "Repository topic"
75
+ option :repos, type: :array, desc: "Explicit repo list"
76
+ option :channels, type: :array, default: [],
77
+ desc: "Filter channels"
78
+ option :stages, type: :array, default: [],
79
+ desc: "Filter stages"
80
+ option :output_dir, type: :string, default: "_site/cc",
81
+ desc: "Output directory"
82
+ option :file_routing, type: :string, default: "by-document",
83
+ desc: "File routing (by-document|flat|by-format)"
84
+ option :cache_dir, type: :string, desc: "Cache directory"
85
+ option :include_drafts, type: :boolean, default: false,
86
+ desc: "Include draft releases"
87
+ option :concurrency, type: :numeric, default: 4
88
+ option :min_documents, type: :numeric, default: 0,
89
+ desc: "Minimum required documents"
90
+ option :token, type: :string, desc: "Platform auth token"
91
+
92
+ def aggregate
93
+ config = AggregateCommand::Config.new(
94
+ source: options[:source],
95
+ organizations: options[:organizations],
96
+ topic: options[:topic],
97
+ repos: options[:repos],
98
+ channels: options[:channels],
99
+ stages: options[:stages],
100
+ output_dir: options[:output_dir],
101
+ file_routing: options[:file_routing],
102
+ cache_dir: options[:cache_dir],
103
+ include_drafts: options[:include_drafts],
104
+ concurrency: options[:concurrency],
105
+ min_documents: options[:min_documents],
106
+ token: options[:token],
107
+ create_zip: nil,
108
+ )
41
109
  result = AggregateCommand.new(config).call
42
110
  print_aggregate_result(result)
43
- if config.min_documents.positive? && result.documents.length < config.min_documents
44
- warn "Error: Found #{result.documents.length} documents, minimum is #{config.min_documents}"
45
- exit 1
46
- end
47
- exit(result.failed_repos.empty? ? 0 : 1)
48
- end
49
111
 
50
- class << self
51
- private
52
-
53
- def parse_package_args(argv)
54
- options = { output_dir: '_site', dest: 'dist', manifest: 'metanorma.release.yml',
55
- config_source: nil }
56
- OptionParser.new do |opts|
57
- opts.banner = 'Usage: metanorma-release package [options]'
58
- opts.on('--output-dir DIR', 'Compiled docs directory') { |v| options[:output_dir] = v }
59
- opts.on('--dest DIR', 'Destination for packages') { |v| options[:dest] = v }
60
- opts.on('--manifest FILE', 'Release manifest file') { |v| options[:manifest] = v }
61
- opts.on('--config SOURCE', 'Channel config (file path or platform ref)') { |v| options[:config_source] = v }
62
- end.parse!(argv)
63
- PackageCommand::Config.new(**options)
112
+ if options[:min_documents].positive? && result.publications.length < options[:min_documents]
113
+ raise PipelineError,
114
+ "Found #{result.publications.length} documents, minimum is #{options[:min_documents]}"
64
115
  end
65
116
 
66
- def parse_publish_args(argv)
67
- options = { output_dir: '_site', platform: 'github', manifest: 'metanorma.release.yml',
68
- force: false, force_replace: [], channels: nil, concurrency: 4, token: nil,
69
- config_source: nil }
70
- OptionParser.new do |opts|
71
- opts.banner = 'Usage: metanorma-release publish [options]'
72
- opts.on('--platform NAME', 'github|local') { |v| options[:platform] = v }
73
- opts.on('--output-dir DIR', 'Compiled docs directory') { |v| options[:output_dir] = v }
74
- opts.on('--manifest FILE', 'Release manifest file') { |v| options[:manifest] = v }
75
- opts.on('--force', 'Force release even if unchanged') { |v| options[:force] = v }
76
- opts.on('--force-replace PAT', 'Glob patterns for force-replace') { |v| options[:force_replace] << v }
77
- opts.on('--channels CHANS', 'Override channels (comma-separated)') { |v| options[:channels] = v.split(',') }
78
- opts.on('--concurrency N', Integer) { |v| options[:concurrency] = v }
79
- opts.on('--token TOKEN', 'Platform auth token') { |v| options[:token] = v }
80
- opts.on('--config SOURCE', 'Channel config (file path or platform ref)') { |v| options[:config_source] = v }
81
- end.parse!(argv)
82
- PublishCommand::Config.new(**options)
117
+ unless result.failed_repos.empty?
118
+ raise PipelineError,
119
+ format_repo_failures(result)
83
120
  end
121
+ end
84
122
 
85
- def parse_aggregate_args(argv)
86
- options = { source: 'github', organizations: [], topic: 'metanorma-release',
87
- repos: nil, channels: [], stages: [], output_dir: '_site/cc',
88
- file_routing: 'by-document', cache_dir: nil,
89
- include_drafts: false, concurrency: 4, min_documents: 0, token: nil }
90
- OptionParser.new do |opts|
91
- opts.banner = 'Usage: metanorma-release aggregate [options]'
92
- opts.on('--source SOURCE', 'github|local:PATH') { |v| options[:source] = v }
93
- opts.on('--organizations ORGS', 'Comma-separated org list') { |v| options[:organizations] = v.split(',') }
94
- opts.on('--topic TOPIC', 'Repository topic') { |v| options[:topic] = v }
95
- opts.on('--repos REPOS', 'Explicit repo list (comma-separated)') { |v| options[:repos] = v.split(',') }
96
- opts.on('--channels CHANS', 'Filter channels (comma-separated)') { |v| options[:channels] = v.split(',') }
97
- opts.on('--stages STAGES', 'Filter stages (comma-separated)') { |v| options[:stages] = v.split(',') }
98
- opts.on('--output-dir DIR', 'Output directory') { |v| options[:output_dir] = v }
99
- opts.on('--file-routing MODE', 'by-document|flat|by-format') { |v| options[:file_routing] = v }
100
- opts.on('--cache-dir DIR', 'Cache directory') { |v| options[:cache_dir] = v }
101
- opts.on('--[no-]include-drafts', 'Include draft releases') { |v| options[:include_drafts] = v }
102
- opts.on('--concurrency N', Integer) { |v| options[:concurrency] = v }
103
- opts.on('--min-documents N', Integer) { |v| options[:min_documents] = v }
104
- opts.on('--token TOKEN', 'Platform auth token') { |v| options[:token] = v }
105
- end.parse!(argv)
106
- AggregateCommand::Config.new(**options)
107
- end
123
+ private
108
124
 
109
- def print_package_result(result, dest)
110
- released = result.released
111
- puts "Packaged #{released.length} documents → #{dest}/"
112
- released.each { |doc| puts " #{doc.id}" }
113
- end
125
+ def print_package_result(result, dest)
126
+ released = result.released
127
+ puts "Packaged #{released.length} documents → #{dest}/"
128
+ released.each { |pub| puts " #{pub.slug}" }
129
+ end
114
130
 
115
- def print_publish_result(result)
116
- puts "Released #{result.released.length}, skipped #{result.skipped.length}, failed #{result.failed.length}"
117
- result.released.each { |doc| puts " RELEASED: #{doc.id} (#{doc.version.tag_component})" }
118
- result.skipped.each { |doc| puts " SKIPPED: #{doc.id} (unchanged)" }
119
- result.failed.each { |f| puts " FAILED: #{f[:document].id} - #{f[:error]}" }
131
+ def print_publish_result(result)
132
+ puts "Released #{result.released.length}, skipped #{result.skipped.length}, failed #{result.failed.length}"
133
+ result.released.each do |pub|
134
+ puts " RELEASED: #{pub.slug} (ed#{pub.edition})"
120
135
  end
121
-
122
- def print_aggregate_result(result)
123
- puts "Aggregated #{result.documents.length} documents from #{result.repo_count} repos"
124
- puts "Index: #{result.channels_found.join(', ')}" unless result.channels_found.empty?
136
+ result.skipped.each { |pub| puts " SKIPPED: #{pub.slug} (unchanged)" }
137
+ result.failed.each do |f|
138
+ puts " FAILED: #{f[:document].slug} - #{f[:error]}"
125
139
  end
126
140
  end
141
+
142
+ def print_aggregate_result(result)
143
+ puts "Aggregated #{result.publications.length} documents from #{result.repo_count} repos"
144
+ puts "Channels: #{result.channels_found.join(', ')}" unless result.channels_found.empty?
145
+ end
146
+
147
+ def format_failures(result)
148
+ result.failed.map do |f|
149
+ "#{f[:document].slug}: #{f[:error]}"
150
+ end.join("\n")
151
+ end
152
+
153
+ def format_repo_failures(result)
154
+ result.failed_repos.map { |r| "#{r.tag}: #{r.message}" }.join("\n")
155
+ end
127
156
  end
128
157
  end
129
158
  end
@@ -6,7 +6,7 @@ module Metanorma
6
6
  Config = Struct.new(
7
7
  :source, :organizations, :topic, :repos, :repo_pattern, :local_path,
8
8
  :channels, :stages, :output_dir, :file_routing, :cache_dir,
9
- :include_drafts, :concurrency, :min_documents, :token, :zip,
9
+ :include_drafts, :concurrency, :min_documents, :token, :create_zip,
10
10
  keyword_init: true
11
11
  )
12
12
 
@@ -16,8 +16,15 @@ module Metanorma
16
16
 
17
17
  def call
18
18
  result = run_aggregation
19
- enrich(result) if result.documents.any?
20
- zip_output if @config.zip
19
+ return result unless result.publications.any?
20
+
21
+ index = build_index(result)
22
+ site = Site.new(index: index, output_dir: @config.output_dir)
23
+ site.write!
24
+ site.enrich!
25
+ site.package! if @config.create_zip
26
+
27
+ stamp_primary_identifiers(index)
21
28
  result
22
29
  end
23
30
 
@@ -29,18 +36,18 @@ module Metanorma
29
36
  organizations: @config.organizations,
30
37
  topic: @config.topic,
31
38
  repos: @config.repos,
32
- token: @config.token
39
+ token: @config.token,
33
40
  )
34
41
 
35
- channel_filter = ChannelFilter.new(
36
- channels: Channel.parse_list(@config.channels)
42
+ metadata_filter = MetadataFilter.new(
43
+ channels: Channel.parse_list(@config.channels),
44
+ stages: @config.stages || [],
37
45
  )
38
- stage_filter = StageFilter.new(@config.stages || [])
39
46
  routing = FileRoutingFactory.from_name(@config.file_routing)
40
47
  asset_processor = AssetProcessor.new(
41
48
  output_dir: @config.output_dir,
42
49
  routing: routing,
43
- canonicalize: true
50
+ canonicalize: true,
44
51
  )
45
52
  delta_state = build_delta_state
46
53
 
@@ -48,10 +55,9 @@ module Metanorma
48
55
  discoverer: adapters[:discoverer],
49
56
  fetcher: adapters[:fetcher],
50
57
  manifest_reader: adapters[:manifest_reader],
51
- channel_filter: channel_filter,
52
- stage_filter: stage_filter,
58
+ metadata_filter: metadata_filter,
53
59
  asset_processor: asset_processor,
54
- delta_state: delta_state
60
+ delta_state: delta_state,
55
61
  )
56
62
 
57
63
  config = AggregationPipeline::Config.new(
@@ -60,66 +66,45 @@ module Metanorma
60
66
  topic: @config.topic,
61
67
  concurrency: @config.concurrency,
62
68
  include_drafts: @config.include_drafts,
63
- fail_on_error: false
69
+ fail_on_error: false,
64
70
  )
65
71
 
66
72
  AggregationPipeline.new(deps).run(config, @config.output_dir)
67
73
  end
68
74
 
69
- def build_delta_state
70
- return NullDeltaState.new unless @config.cache_dir
71
-
72
- DeltaState.new(
73
- cache_store: FileCacheStore.new(@config.cache_dir),
74
- output_dir: @config.output_dir
75
- )
76
- end
77
-
78
- def enrich(result)
79
- index = DocumentIndex.from_documents(
80
- result.documents,
81
- parameters: IndexParameters.new(
75
+ def build_index(result)
76
+ Index.from_documents(
77
+ result.publications,
78
+ parameters: {
82
79
  organizations: @config.organizations,
83
80
  channels: @config.channels || [],
84
81
  topic: @config.topic,
85
- repo_count: result.repo_count
86
- )
82
+ repo_count: result.repo_count,
83
+ },
87
84
  )
88
- enricher = RelatonEnricher.new
89
- enrich_result = enricher.enrich(index, @config.output_dir)
90
- return unless enrich_result
91
-
92
- stamp_primary_identifiers(enrich_result.documents)
93
- rescue LoadError
94
- warn ' (relaton gem not available — bibliography skipped)'
95
85
  end
96
86
 
97
- def stamp_primary_identifiers(documents)
98
- documents.map do |doc|
99
- next doc unless doc['bibliographic']
100
-
101
- ids = doc['bibliographic']['docidentifier']
102
- next doc unless ids&.any?
87
+ def build_delta_state
88
+ return NullDeltaState.new unless @config.cache_dir
103
89
 
104
- primary = ids.find { |di| di['primary'] == true } || ids.first
105
- doc.merge('primary_identifier' => primary['content'])
106
- end
90
+ DeltaState.new(
91
+ cache_store: FileCacheStore.new(@config.cache_dir),
92
+ output_dir: @config.output_dir,
93
+ )
107
94
  end
108
95
 
109
- def zip_output
110
- require 'zip'
96
+ def stamp_primary_identifiers(index)
97
+ index.publications.each do |pub|
98
+ next unless pub.to_h["bibliographic"]
111
99
 
112
- dir = @config.output_dir
113
- zip_path = "#{dir}.zip"
100
+ ids = pub.to_h.dig("bibliographic", "docidentifier")
101
+ next unless ids&.any?
114
102
 
115
- Zip::File.open(zip_path, Zip::File::CREATE) do |zipfile|
116
- Dir.glob("#{dir}/**/*").each do |file|
117
- next if File.directory?(file)
118
-
119
- entry_name = file.sub("#{File.dirname(dir)}/", '')
120
- zipfile.add(entry_name, file)
121
- end
103
+ primary = ids.find { |di| di["primary"] == true } || ids.first
104
+ pub.to_h.merge("primary_identifier" => primary["content"])
122
105
  end
106
+ rescue LoadError
107
+ warn " (relaton gem not available — bibliography skipped)"
123
108
  end
124
109
  end
125
110
  end
@@ -8,26 +8,22 @@ module Metanorma
8
8
  keyword_init: true
9
9
  )
10
10
 
11
- include ConfigResolver
12
-
13
11
  def initialize(config)
14
12
  @config = config
15
13
  end
16
14
 
17
15
  def call
18
- manifest = load_manifest(@config.manifest)
19
- channel_config = resolve_channel_config(@config.config_source, manifest)
20
-
16
+ config = load_config
21
17
  deps = ReleasePipeline::Dependencies.new(
22
- extractor: RxlExtractor.new,
18
+ extractor: Publication,
23
19
  filters: [],
24
20
  change_detector: ContentHashChangeDetector.new(previous_releases: {}),
25
- packager: ZipPackager.new,
26
- publisher: PlatformFactory.build_publisher('null', {}),
27
- naming_registry: NamingRegistry.default_registry,
28
- manifest: manifest,
21
+ packager: ZipPackager.new(output_dir: @config.output_dir),
22
+ publisher: PlatformFactory.build_publisher("null", {}),
23
+ slug_registry: SlugRegistry.from_config(config),
24
+ manifest: nil,
29
25
  channel_override: nil,
30
- channel_config: channel_config
26
+ config: config,
31
27
  )
32
28
 
33
29
  pipeline_config = ReleasePipeline::Config.new(
@@ -36,11 +32,23 @@ module Metanorma
36
32
  force: false,
37
33
  force_replace_patterns: nil,
38
34
  concurrency: 4,
39
- default_visibility: 'public'
35
+ default_visibility: "public",
40
36
  )
41
37
 
42
38
  ReleasePipeline.new(deps).run(pipeline_config)
43
39
  end
40
+
41
+ private
42
+
43
+ def load_config
44
+ if @config.config_source && File.exist?(@config.config_source)
45
+ Metanorma::Release::Config.from_file(@config.config_source)
46
+ elsif @config.manifest && File.exist?(@config.manifest)
47
+ Metanorma::Release::Config.from_file(@config.manifest)
48
+ else
49
+ Metanorma::Release::Config.defaults
50
+ end
51
+ end
44
52
  end
45
53
  end
46
54
  end
@@ -2,37 +2,33 @@
2
2
 
3
3
  module Metanorma
4
4
  module Release
5
- class PublishCommand
5
+ class ReleaseCommand
6
6
  Config = Struct.new(
7
7
  :output_dir, :platform, :manifest, :force,
8
8
  :force_replace, :channels, :concurrency, :token, :config_source,
9
9
  keyword_init: true
10
10
  )
11
11
 
12
- include ConfigResolver
13
-
14
12
  def initialize(config)
15
13
  @config = config
16
14
  end
17
15
 
18
16
  def call
19
- manifest = load_manifest(@config.manifest)
20
- channel_config = resolve_channel_config(@config.config_source, manifest)
21
-
17
+ config = load_config
22
18
  options = { token: @config.token }
23
19
  publisher = PlatformFactory.build_publisher(@config.platform, options)
24
20
  channel_override = Channel.parse_list(@config.channels) if @config.channels
25
21
 
26
22
  deps = ReleasePipeline::Dependencies.new(
27
- extractor: RxlExtractor.new,
23
+ extractor: Publication,
28
24
  filters: [],
29
25
  change_detector: ContentHashChangeDetector.new(previous_releases: {}),
30
- packager: ZipPackager.new,
26
+ packager: ZipPackager.new(output_dir: @config.output_dir),
31
27
  publisher: publisher,
32
- naming_registry: NamingRegistry.default_registry,
33
- manifest: manifest,
28
+ slug_registry: SlugRegistry.from_config(config),
29
+ manifest: nil,
34
30
  channel_override: channel_override,
35
- channel_config: channel_config
31
+ config: config,
36
32
  )
37
33
 
38
34
  pipeline_config = ReleasePipeline::Config.new(
@@ -41,11 +37,23 @@ module Metanorma
41
37
  force: @config.force,
42
38
  force_replace_patterns: @config.force_replace && !@config.force_replace.empty? ? @config.force_replace : nil,
43
39
  concurrency: @config.concurrency,
44
- default_visibility: 'public'
40
+ default_visibility: "public",
45
41
  )
46
42
 
47
43
  ReleasePipeline.new(deps).run(pipeline_config)
48
44
  end
45
+
46
+ private
47
+
48
+ def load_config
49
+ if @config.config_source && File.exist?(@config.config_source)
50
+ Metanorma::Release::Config.from_file(@config.config_source)
51
+ elsif @config.manifest && File.exist?(@config.manifest)
52
+ Metanorma::Release::Config.from_file(@config.manifest)
53
+ else
54
+ Metanorma::Release::Config.defaults
55
+ end
56
+ end
49
57
  end
50
58
  end
51
59
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Metanorma
6
+ module Release
7
+ class Config
8
+ def self.from_yaml(yaml_string)
9
+ data = YAML.safe_load(yaml_string, permitted_classes: [Symbol])
10
+ new(data || {})
11
+ end
12
+
13
+ def self.from_file(path)
14
+ unless File.exist?(path)
15
+ raise ArgumentError,
16
+ "Config file not found: #{path}"
17
+ end
18
+
19
+ from_yaml(File.read(path))
20
+ end
21
+
22
+ def self.defaults
23
+ new({})
24
+ end
25
+
26
+ def initialize(data)
27
+ @data = data
28
+ end
29
+
30
+ def channels
31
+ @data.fetch("channels", [])
32
+ end
33
+
34
+ def routing
35
+ @data.fetch("routing", {})
36
+ end
37
+
38
+ def routing_default
39
+ routing.fetch("default", ["public"])
40
+ end
41
+
42
+ def routing_rules
43
+ routing.fetch("rules", [])
44
+ end
45
+
46
+ def slug_config
47
+ @data.fetch("slug", {})
48
+ end
49
+
50
+ def slug_default_strategy
51
+ slug_config.fetch("default", "edition")
52
+ end
53
+
54
+ def slug_strategies
55
+ slug_config.fetch("strategies", {})
56
+ end
57
+
58
+ def documents
59
+ @data.fetch("documents", [])
60
+ end
61
+
62
+ def defaults
63
+ @data.fetch("defaults", {})
64
+ end
65
+
66
+ def default_channels
67
+ list = defaults.fetch("channels", nil)
68
+ return ["public"] unless list
69
+
70
+ list
71
+ end
72
+
73
+ def resolve_channels(publication)
74
+ manifest_channels = resolve_manifest_channels(publication)
75
+ return manifest_channels if manifest_channels
76
+
77
+ rule_channels = resolve_routing_rules(publication)
78
+ return rule_channels if rule_channels
79
+
80
+ default_channels
81
+ end
82
+
83
+ private
84
+
85
+ def resolve_manifest_channels(publication)
86
+ documents.each do |entry|
87
+ next unless entry["source"] && publication.source_path&.end_with?(entry["source"])
88
+ return entry["channels"] if entry["channels"]
89
+ end
90
+ nil
91
+ end
92
+
93
+ def resolve_routing_rules(publication)
94
+ routing_rules.each do |rule|
95
+ match = true
96
+ match &&= Array(rule["stage"]).map(&:to_s).include?(publication.stage.to_s) if rule["stage"]
97
+ match &&= Array(rule["doctype"]).map(&:to_s).include?(publication.doctype.to_s) if rule["doctype"]
98
+ return rule["channels"] if match && rule["channels"]
99
+ end
100
+ nil
101
+ end
102
+ end
103
+ end
104
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'digest'
3
+ require "digest"
4
4
 
5
5
  module Metanorma
6
6
  module Release
@@ -25,8 +25,16 @@ module Metanorma
25
25
  end
26
26
 
27
27
  def self.of_directory(directory, base: nil)
28
- pattern = base ? File.join(directory, "#{base}.*") : File.join(directory, '**', '*')
29
- files = Dir.glob(pattern).reject { |f| File.directory?(f) || f.end_with?('.zip') }
28
+ pattern = if base
29
+ File.join(directory,
30
+ "#{base}.*")
31
+ else
32
+ File.join(directory, "**",
33
+ "*")
34
+ end
35
+ files = Dir.glob(pattern).reject do |f|
36
+ File.directory?(f) || f.end_with?(".zip")
37
+ end
30
38
  of_files(files)
31
39
  end
32
40