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
@@ -2,13 +2,42 @@
2
2
 
3
3
  module Metanorma
4
4
  module Release
5
- class AggregationPipeline
5
+ FetchResult = Struct.new(:releases, :etag, :unchanged?, keyword_init: true)
6
+ RepoReport = Struct.new(:releases, :included, :skipped, :reason, :errors,
7
+ keyword_init: true)
8
+ RepoError = Struct.new(:tag, :message, keyword_init: true)
9
+
10
+ class AggregationPipeline # rubocop:disable Metrics/ClassLength
6
11
  Dependencies = Struct.new(
7
12
  :discoverer, :fetcher, :manifest_reader,
8
- :channel_filter, :stage_filter,
9
- :asset_processor, :delta_state,
13
+ :metadata_filter, :asset_processor, :delta_state,
10
14
  keyword_init: true
11
- )
15
+ ) do
16
+ def initialize(**kwargs)
17
+ super
18
+ validate_types!
19
+ end
20
+
21
+ private
22
+
23
+ def validate_types!
24
+ validate_interface!(discoverer, RepoDiscoverer, "discoverer")
25
+ validate_interface!(fetcher, ReleaseFetcher, "fetcher")
26
+ validate_interface!(manifest_reader, ManifestReader,
27
+ "manifest_reader")
28
+ validate_interface!(delta_state, DeltaStateManager, "delta_state")
29
+ end
30
+
31
+ def validate_interface!(obj, mod, name)
32
+ return if obj.is_a?(mod) || begin
33
+ obj.class.ancestors.include?(mod)
34
+ rescue StandardError
35
+ false
36
+ end
37
+
38
+ raise ArgumentError, "#{name} must include #{mod}, got #{obj.class}"
39
+ end
40
+ end
12
41
 
13
42
  Config = Struct.new(
14
43
  :organizations, :channels, :topic,
@@ -17,7 +46,7 @@ module Metanorma
17
46
  )
18
47
 
19
48
  Result = Struct.new(
20
- :documents, :repo_count, :channels_found,
49
+ :publications, :repo_count, :channels_found,
21
50
  :report, :failed_repos,
22
51
  keyword_init: true
23
52
  )
@@ -29,13 +58,13 @@ module Metanorma
29
58
  def run(config, output_dir)
30
59
  @deps.delta_state.load
31
60
  repos = @deps.discoverer.discover
32
- documents = []
61
+ publications = []
33
62
  reports = []
34
63
  failed_repos = []
35
64
 
36
65
  repos.each do |repo|
37
66
  repo_docs, report = process_repo(repo, output_dir, config)
38
- documents.concat(repo_docs)
67
+ publications.concat(repo_docs)
39
68
  reports << report
40
69
  rescue StandardError => e
41
70
  failed_repos << RepoError.new(tag: repo.to_s, message: e.message)
@@ -45,11 +74,11 @@ module Metanorma
45
74
  @deps.delta_state.save
46
75
 
47
76
  Result.new(
48
- documents: documents,
77
+ publications: publications,
49
78
  repo_count: repos.length,
50
- channels_found: documents.flat_map { |d| d.channels || [] }.uniq.sort,
79
+ channels_found: publications.flat_map(&:channels).uniq.sort,
51
80
  report: reports,
52
- failed_repos: failed_repos
81
+ failed_repos: failed_repos,
53
82
  )
54
83
  end
55
84
 
@@ -59,9 +88,9 @@ module Metanorma
59
88
  repo_key = repo.to_s
60
89
 
61
90
  manifest_channels = @deps.manifest_reader.read(repo)
62
- if manifest_channels && !@deps.channel_filter.overlaps?(manifest_channels)
91
+ if manifest_channels && !@deps.metadata_filter.overlaps?(manifest_channels)
63
92
  return [], RepoReport.new(releases: 0, included: 0, skipped: 0,
64
- reason: 'channel manifest', errors: [])
93
+ reason: "channel manifest", errors: [])
65
94
  end
66
95
 
67
96
  etag = @deps.delta_state.etag(repo_key)
@@ -69,19 +98,19 @@ module Metanorma
69
98
 
70
99
  if fetch_result.unchanged?
71
100
  return [], RepoReport.new(releases: 0, included: 0, skipped: 0,
72
- reason: 'etag unchanged', errors: [])
101
+ reason: "etag unchanged", errors: [])
73
102
  end
74
103
 
75
104
  current_tags = []
76
- documents = []
105
+ publications = []
77
106
  errors = []
78
107
 
79
108
  fetch_result.releases.each do |release|
80
- metadata = ReleaseMetadata.from_release_body(release.body)
109
+ metadata = Publication.from_release_body(release.body)
81
110
  next if metadata.nil?
82
111
 
83
- next unless @deps.channel_filter.matches?(metadata.to_h)
84
- next unless @deps.stage_filter.matches?(metadata.to_h)
112
+ metadata_h = metadata.to_h
113
+ next unless @deps.metadata_filter.matches?(metadata_h)
85
114
  next if release.prerelease && !config.include_drafts
86
115
 
87
116
  tag = release.tag_name
@@ -91,16 +120,19 @@ module Metanorma
91
120
 
92
121
  if @deps.delta_state.processed?(repo_key, tag, content_hash)
93
122
  files = @deps.delta_state.release_files(repo_key, tag)
94
- documents << build_document(metadata, files, content_hash, release, repo)
123
+ publications << build_publication(metadata, files, content_hash,
124
+ release, repo)
95
125
  next
96
126
  end
97
127
 
98
128
  zip_asset = find_zip_asset(release)
99
129
  next unless zip_asset
100
130
 
101
- result = @deps.asset_processor.process(zip_asset.data, metadata.to_h)
102
- @deps.delta_state.mark_processed(repo_key, tag, content_hash, result.files.map(&:path))
103
- documents << build_document(metadata, result.files.map(&:path), content_hash, release, repo)
131
+ result = @deps.asset_processor.process(zip_asset.data, metadata_h)
132
+ @deps.delta_state.mark_processed(repo_key, tag, content_hash,
133
+ result.files.map(&:path))
134
+ publications << build_publication(metadata, result.files.map(&:path),
135
+ content_hash, release, repo)
104
136
  rescue StandardError => e
105
137
  errors << RepoError.new(tag: release.tag_name, message: e.message)
106
138
  end
@@ -108,34 +140,16 @@ module Metanorma
108
140
  @deps.delta_state.cleanup_stale(repo_key, current_tags)
109
141
  @deps.delta_state.set_etag(repo_key, fetch_result.etag)
110
142
 
111
- [documents, RepoReport.new(
143
+ [publications, RepoReport.new(
112
144
  releases: fetch_result.releases.length,
113
- included: documents.length,
114
- skipped: fetch_result.releases.length - documents.length,
145
+ included: publications.length,
146
+ skipped: fetch_result.releases.length - publications.length,
115
147
  reason: nil, errors: errors
116
148
  )]
117
149
  end
118
150
 
119
- def build_document(metadata, files, content_hash, release, repo)
120
- source = DocumentSource.new(
121
- owner: repo.owner, repo: repo.repo,
122
- tag: release.tag_name,
123
- release_url: release.html_url,
124
- release_date: release.published_at
125
- )
126
-
127
- file_structs = files.map { |f| DocumentFile.new(name: File.basename(f), path: f) }
128
-
129
- AggregatedDocument.new(
130
- id: metadata.id, title: metadata.title,
131
- edition: metadata.edition, stage: metadata.stage,
132
- doctype: metadata.doctype,
133
- channels: metadata.channels,
134
- formats: metadata.formats,
135
- flavor: metadata.flavor,
136
- content_hash: content_hash.to_s,
137
- source: source, files: file_structs
138
- )
151
+ def build_publication(metadata, files, _content_hash, release, repo)
152
+ metadata.with_files_and_source(files, release, repo)
139
153
  end
140
154
 
141
155
  def extract_content_hash(body)
@@ -148,7 +162,7 @@ module Metanorma
148
162
  def find_zip_asset(release)
149
163
  return nil unless release.assets
150
164
 
151
- release.assets.find { |a| a.name.end_with?('.zip') }
165
+ release.assets.find { |a| a.name.end_with?(".zip") }
152
166
  end
153
167
  end
154
168
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  begin
4
- require 'zip'
4
+ require "zip"
5
5
  rescue LoadError
6
- raise LoadError, "The rubyzip gem is required for AssetProcessor. Add `gem 'rubyzip'` to your Gemfile."
6
+ raise LoadError,
7
+ "The rubyzip gem is required for AssetProcessor. Add `gem 'rubyzip'` to your Gemfile."
7
8
  end
8
9
 
9
10
  module Metanorma
@@ -23,7 +24,7 @@ module Metanorma
23
24
  files = []
24
25
 
25
26
  Dir.mktmpdir do |tmp_dir|
26
- zip_path = File.join(tmp_dir, 'archive.zip')
27
+ zip_path = File.join(tmp_dir, "archive.zip")
27
28
  File.binwrite(zip_path, zip_data)
28
29
 
29
30
  Zip::File.open(zip_path) do |zip_file|
@@ -38,20 +39,21 @@ module Metanorma
38
39
  FileUtils.mkdir_p(File.dirname(dest_path))
39
40
  entry.extract(dest_path) { true }
40
41
 
41
- files << DocumentFile.new(name: file_name, path: relative_path)
42
+ ext = File.extname(file_name).delete_prefix(".")
43
+ files << PublicationFile.new(format: ext, name: file_name,
44
+ path: relative_path)
42
45
  end
43
46
  end
44
47
  end
45
48
 
46
- ProcessResult.new(files: files, channels: metadata['channels'])
49
+ ProcessResult.new(files: files, channels: metadata["channels"])
47
50
  end
48
51
 
49
52
  private
50
53
 
51
54
  def canonicalize_name(name)
52
- # Strip edition suffix: -ed1. → ., -ed1-wd. -wd.
53
- name.sub(/-ed\d+(\.\d+)?-(?=[a-z0-9])/, '-')
54
- .sub(/-ed\d+(\.\d+)?\./, '.')
55
+ name.sub(/-ed\d+(\.\d+)?-(?=[a-z0-9])/, "-")
56
+ .sub(/-ed\d+(\.\d+)?\./, ".")
55
57
  end
56
58
  end
57
59
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
3
+ require "json"
4
4
 
5
5
  module Metanorma
6
6
  module Release
@@ -47,13 +47,13 @@ module Metanorma
47
47
 
48
48
  def delete(key)
49
49
  path = file_path(key)
50
- File.delete(path) if File.exist?(path)
50
+ FileUtils.rm_f(path)
51
51
  end
52
52
 
53
53
  def clear
54
54
  return unless Dir.exist?(@directory)
55
55
 
56
- Dir.glob(File.join(@directory, '*')).each do |f|
56
+ Dir.glob(File.join(@directory, "*")).each do |f|
57
57
  File.delete(f) if File.file?(f)
58
58
  end
59
59
  end
@@ -61,14 +61,14 @@ module Metanorma
61
61
  def keys
62
62
  return [] unless Dir.exist?(@directory)
63
63
 
64
- Dir.glob(File.join(@directory, '*')).select { |f| File.file?(f) }
65
- .map { |f| File.basename(f) }
64
+ Dir.glob(File.join(@directory, "*")).select { |f| File.file?(f) }
65
+ .map { |f| File.basename(f) }
66
66
  end
67
67
 
68
68
  private
69
69
 
70
70
  def file_path(key)
71
- sanitized = key.gsub(/[^a-zA-Z0-9._-]/, '_')
71
+ sanitized = key.gsub(/[^a-zA-Z0-9._-]/, "_")
72
72
  File.join(@directory, sanitized)
73
73
  end
74
74
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Metanorma
4
4
  module Release
5
+ ChangeResult = Struct.new(:changed?, :current_hash, :previous_hash,
6
+ keyword_init: true)
7
+
5
8
  class ContentHashChangeDetector
6
9
  include ChangeDetector
7
10
 
@@ -9,11 +12,12 @@ module Metanorma
9
12
  @previous_releases = previous_releases
10
13
  end
11
14
 
12
- def detect(metadata, tag, force: false)
13
- current = ContentHash.of_directory(metadata.output_dir, base: metadata.file_base_name)
15
+ def detect(publication, tag, force: false)
16
+ current = publication.content_hash
14
17
  previous = @previous_releases[tag.to_s]
15
18
  changed = force || previous.nil? || !current.eql?(previous)
16
- ChangeResult.new(changed?: changed, current_hash: current, previous_hash: previous)
19
+ ChangeResult.new(changed?: changed, current_hash: current,
20
+ previous_hash: previous)
17
21
  end
18
22
  end
19
23
  end
@@ -3,61 +3,35 @@
3
3
  module Metanorma
4
4
  module Release
5
5
  class Channel
6
- def self.parse(channel_string)
7
- parts = channel_string.to_s.strip.split('/', 2)
8
- if ChannelAudience.values.include?(parts[0])
9
- new(audience: parts[0], category: parts[1] || 'default')
10
- else
11
- new(audience: ChannelAudience::PUBLIC, category: parts[0])
12
- end
13
- end
14
-
15
- def self.parse_list(strings)
16
- (strings || []).map { |s| parse(s) }
17
- end
18
-
19
- def self.public(category)
20
- new(audience: ChannelAudience::PUBLIC, category: category)
21
- end
22
-
23
- def self.members(category)
24
- new(audience: ChannelAudience::MEMBERS, category: category)
25
- end
6
+ attr_reader :name
26
7
 
27
- def self.internal(category)
28
- new(audience: ChannelAudience::INTERNAL, category: category)
29
- end
30
-
31
- attr_reader :audience, :category
32
-
33
- def initialize(audience:, category:)
34
- @audience = audience
35
- @category = category
8
+ def initialize(name)
9
+ @name = name.to_s.strip
36
10
  freeze
37
11
  end
38
12
 
39
13
  def to_s
40
- "#{audience}/#{category}"
14
+ @name
41
15
  end
42
16
 
43
- def public?
44
- audience == ChannelAudience::PUBLIC
17
+ def eql?(other)
18
+ other.is_a?(self.class) && @name == other.name
45
19
  end
46
20
 
47
- def members?
48
- audience == ChannelAudience::MEMBERS
21
+ def hash
22
+ @name.hash
49
23
  end
50
24
 
51
25
  def matches?(filter_channels)
52
- filter_channels.any? { |c| eql?(Channel.parse(c)) }
26
+ filter_channels.any? { |c| eql?(Channel.new(c)) }
53
27
  end
54
28
 
55
- def eql?(other)
56
- other.is_a?(self.class) && audience == other.audience && category == other.category
29
+ def self.parse(channel_string)
30
+ new(channel_string.to_s.strip)
57
31
  end
58
32
 
59
- def hash
60
- [audience, category].hash
33
+ def self.parse_list(strings)
34
+ (strings || []).map { |s| parse(s) }
61
35
  end
62
36
  end
63
37
  end
@@ -2,25 +2,41 @@
2
2
 
3
3
  module Metanorma
4
4
  module Release
5
- class ChannelFilter
6
- def initialize(channels)
7
- @channels = channels.map { |c| Channel.parse(c) }
8
- @all = @channels.empty?
5
+ class MetadataFilter
6
+ def initialize(channels: [], stages: [])
7
+ @channels = channels.map { |c| Channel.new(c) }
8
+ @stages = Set.new(stages.map(&:downcase))
9
+ @all_channels = @channels.empty?
10
+ @all_stages = @stages.empty?
9
11
  end
10
12
 
11
13
  def matches?(release_metadata)
12
- return true if @all
13
-
14
- release_channels = (release_metadata['channels'] || []).map { |c| Channel.parse(c) }
15
- release_channels.any? { |rc| @channels.any? { |fc| fc.eql?(rc) } }
14
+ channel_match?(release_metadata) && stage_match?(release_metadata)
16
15
  end
17
16
 
18
17
  def overlaps?(manifest_channels)
19
- return true if @all
18
+ return true if @all_channels
20
19
 
21
- parsed = manifest_channels.map { |c| Channel.parse(c) }
20
+ parsed = manifest_channels.map { |c| Channel.new(c) }
22
21
  parsed.any? { |mc| @channels.any? { |fc| fc.eql?(mc) } }
23
22
  end
23
+
24
+ private
25
+
26
+ def channel_match?(release_metadata)
27
+ return true if @all_channels
28
+
29
+ release_channels = (release_metadata["channels"] || []).map do |c|
30
+ Channel.new(c)
31
+ end
32
+ release_channels.any? { |rc| @channels.any? { |fc| fc.eql?(rc) } }
33
+ end
34
+
35
+ def stage_match?(release_metadata)
36
+ return true if @all_stages
37
+
38
+ @stages.include?(release_metadata["stage"].to_s.downcase)
39
+ end
24
40
  end
25
41
  end
26
42
  end