metanorma-release 0.2.24 → 0.2.26
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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +21 -319
- data/README.adoc +319 -92
- data/lib/metanorma/release/aggregation_pipeline.rb +128 -45
- data/lib/metanorma/release/asset_processor.rb +3 -3
- data/lib/metanorma/release/cache_store.rb +1 -0
- data/lib/metanorma/release/change_detector.rb +3 -2
- data/lib/metanorma/release/cli.rb +28 -4
- data/lib/metanorma/release/commands/aggregate.rb +1 -17
- data/lib/metanorma/release/commands/package.rb +9 -17
- data/lib/metanorma/release/commands/release_command.rb +19 -14
- data/lib/metanorma/release/config.rb +16 -7
- data/lib/metanorma/release/delta_state.rb +15 -1
- data/lib/metanorma/release/dependency_validation.rb +19 -0
- data/lib/metanorma/release/document_flattener.rb +173 -0
- data/lib/metanorma/release/index.rb +1 -1
- data/lib/metanorma/release/interfaces.rb +12 -0
- data/lib/metanorma/release/platform/github/manifest_reader.rb +3 -1
- data/lib/metanorma/release/platform/github/release_fetcher.rb +10 -14
- data/lib/metanorma/release/platform/github/topic_discoverer.rb +1 -1
- data/lib/metanorma/release/platform/github.rb +0 -4
- data/lib/metanorma/release/platform/local/fetcher.rb +5 -17
- data/lib/metanorma/release/platform/null/manifest_reader.rb +15 -0
- data/lib/metanorma/release/platform/null/publisher.rb +1 -1
- data/lib/metanorma/release/platform/null.rb +2 -0
- data/lib/metanorma/release/platform/static_discoverer.rb +19 -0
- data/lib/metanorma/release/platform.rb +1 -0
- data/lib/metanorma/release/platform_factory.rb +6 -21
- data/lib/metanorma/release/publication.rb +23 -161
- data/lib/metanorma/release/publication_serializer.rb +59 -0
- data/lib/metanorma/release/release_pipeline.rb +7 -15
- data/lib/metanorma/release/rxl_extractor.rb +106 -0
- data/lib/metanorma/release/site.rb +5 -165
- data/lib/metanorma/release/slug_strategy.rb +30 -15
- data/lib/metanorma/release/version.rb +1 -1
- data/lib/metanorma/release/zip_packager.rb +1 -1
- data/lib/metanorma/release.rb +36 -19
- metadata +11 -5
|
@@ -2,17 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
module Metanorma
|
|
4
4
|
module Release
|
|
5
|
-
|
|
5
|
+
# failed?: the release listing itself failed (rate limit, network) —
|
|
6
|
+
# distinct from an empty result. A failed listing must never be
|
|
7
|
+
# treated as "all releases deleted".
|
|
8
|
+
FetchResult = Struct.new(:releases, :etag, :unchanged?, :failed?,
|
|
9
|
+
keyword_init: true) do
|
|
10
|
+
def initialize(releases:, etag:, **flags)
|
|
11
|
+
super(releases: releases, etag: etag,
|
|
12
|
+
unchanged?: flags.fetch(:unchanged?, false),
|
|
13
|
+
failed?: flags.fetch(:failed?, false))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
6
16
|
RepoReport = Struct.new(:releases, :included, :skipped, :reason, :errors,
|
|
7
17
|
keyword_init: true)
|
|
8
18
|
RepoError = Struct.new(:tag, :message, keyword_init: true)
|
|
19
|
+
Collision = Struct.new(:path, :claimed_by, keyword_init: true)
|
|
9
20
|
|
|
10
|
-
class AggregationPipeline
|
|
21
|
+
class AggregationPipeline
|
|
11
22
|
Dependencies = Struct.new(
|
|
12
23
|
:discoverer, :fetcher, :manifest_reader,
|
|
13
24
|
:metadata_filter, :asset_processor, :delta_state,
|
|
14
25
|
keyword_init: true
|
|
15
26
|
) do
|
|
27
|
+
include DependencyValidation
|
|
28
|
+
|
|
16
29
|
def initialize(**kwargs)
|
|
17
30
|
super
|
|
18
31
|
validate_types!
|
|
@@ -27,16 +40,6 @@ module Metanorma
|
|
|
27
40
|
"manifest_reader")
|
|
28
41
|
validate_interface!(delta_state, DeltaStateManager, "delta_state")
|
|
29
42
|
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
43
|
end
|
|
41
44
|
|
|
42
45
|
Config = Struct.new(
|
|
@@ -47,9 +50,14 @@ module Metanorma
|
|
|
47
50
|
|
|
48
51
|
Result = Struct.new(
|
|
49
52
|
:publications, :repo_count, :channels_found,
|
|
50
|
-
:report, :failed_repos,
|
|
53
|
+
:report, :failed_repos, :collisions,
|
|
51
54
|
keyword_init: true
|
|
52
|
-
)
|
|
55
|
+
) do
|
|
56
|
+
def initialize(publications:, repo_count:, channels_found:, report:,
|
|
57
|
+
failed_repos:, collisions: [])
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
end
|
|
53
61
|
|
|
54
62
|
def initialize(deps)
|
|
55
63
|
@deps = deps
|
|
@@ -58,17 +66,13 @@ module Metanorma
|
|
|
58
66
|
def run(config, output_dir)
|
|
59
67
|
@deps.delta_state.load
|
|
60
68
|
repos = @deps.discoverer.discover
|
|
61
|
-
publications = []
|
|
62
|
-
reports = []
|
|
63
|
-
failed_repos = []
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
raise if config.fail_on_error
|
|
70
|
+
if config.concurrency > 1
|
|
71
|
+
publications, reports, failed_repos = run_concurrent(repos,
|
|
72
|
+
output_dir, config)
|
|
73
|
+
else
|
|
74
|
+
publications, reports, failed_repos = run_sequential(repos,
|
|
75
|
+
output_dir, config)
|
|
72
76
|
end
|
|
73
77
|
|
|
74
78
|
@deps.delta_state.save
|
|
@@ -79,11 +83,73 @@ module Metanorma
|
|
|
79
83
|
channels_found: publications.flat_map(&:channels).uniq.sort,
|
|
80
84
|
report: reports,
|
|
81
85
|
failed_repos: failed_repos,
|
|
86
|
+
collisions: detect_collisions(publications),
|
|
82
87
|
)
|
|
83
88
|
end
|
|
84
89
|
|
|
90
|
+
# Two releases extracting to the same path (same slug from
|
|
91
|
+
# different repos, or same-year editions under flat routing)
|
|
92
|
+
# silently overwrite each other. Surface them so callers can
|
|
93
|
+
# disambiguate in source repos or fail loudly.
|
|
94
|
+
def detect_collisions(publications)
|
|
95
|
+
claims = Hash.new { |h, k| h[k] = [] }
|
|
96
|
+
publications.each do |pub|
|
|
97
|
+
owner = pub.source ? "#{pub.source.repo_key}@#{pub.source.tag}" : pub.slug
|
|
98
|
+
pub.files.each { |file| claims[file.path] << owner }
|
|
99
|
+
end
|
|
100
|
+
claims.select { |_path, owners| owners.uniq.length > 1 }.map do |path, owners|
|
|
101
|
+
Collision.new(path: path, claimed_by: owners.uniq)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
85
105
|
private
|
|
86
106
|
|
|
107
|
+
def run_sequential(repos, output_dir, config)
|
|
108
|
+
publications = []
|
|
109
|
+
reports = []
|
|
110
|
+
failed_repos = []
|
|
111
|
+
|
|
112
|
+
repos.each do |repo|
|
|
113
|
+
repo_docs, report = process_repo(repo, output_dir, config)
|
|
114
|
+
publications.concat(repo_docs)
|
|
115
|
+
reports << report
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
failed_repos << RepoError.new(tag: repo.to_s, message: e.message)
|
|
118
|
+
raise if config.fail_on_error
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
[publications, reports, failed_repos]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def run_concurrent(repos, output_dir, config)
|
|
125
|
+
publications = []
|
|
126
|
+
reports = []
|
|
127
|
+
failed_repos = []
|
|
128
|
+
mutex = Mutex.new
|
|
129
|
+
threads = repos.each_slice([
|
|
130
|
+
(repos.length.to_f / config.concurrency).ceil, 1
|
|
131
|
+
].max).map do |batch|
|
|
132
|
+
Thread.new(batch) do |slice|
|
|
133
|
+
slice.each do |repo|
|
|
134
|
+
repo_docs, report = process_repo(repo, output_dir, config)
|
|
135
|
+
mutex.synchronize do
|
|
136
|
+
publications.concat(repo_docs)
|
|
137
|
+
reports << report
|
|
138
|
+
end
|
|
139
|
+
rescue StandardError => e
|
|
140
|
+
mutex.synchronize do
|
|
141
|
+
failed_repos << RepoError.new(tag: repo.to_s,
|
|
142
|
+
message: e.message)
|
|
143
|
+
end
|
|
144
|
+
raise if config.fail_on_error
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
threads.each { |t| t.join(300) }
|
|
149
|
+
|
|
150
|
+
[publications, reports, failed_repos]
|
|
151
|
+
end
|
|
152
|
+
|
|
87
153
|
def process_repo(repo, output_dir, config)
|
|
88
154
|
repo_key = repo.to_s
|
|
89
155
|
|
|
@@ -96,6 +162,17 @@ module Metanorma
|
|
|
96
162
|
etag = @deps.delta_state.etag(repo_key)
|
|
97
163
|
fetch_result = @deps.fetcher.fetch(repo, etag: etag)
|
|
98
164
|
|
|
165
|
+
if fetch_result.failed?
|
|
166
|
+
# The listing failed (rate limit, network) — this is NOT "the
|
|
167
|
+
# releases are gone". Keep the existing extracted files and
|
|
168
|
+
# delta state untouched so a later run can reconcile.
|
|
169
|
+
return [], RepoReport.new(
|
|
170
|
+
releases: 0, included: 0, skipped: 0,
|
|
171
|
+
reason: "fetch failed — existing state kept",
|
|
172
|
+
errors: [RepoError.new(tag: nil, message: "release listing failed")]
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
99
176
|
if fetch_result.unchanged?
|
|
100
177
|
return [], RepoReport.new(releases: 0, included: 0, skipped: 0,
|
|
101
178
|
reason: "etag unchanged", errors: [])
|
|
@@ -116,25 +193,27 @@ module Metanorma
|
|
|
116
193
|
tag = release.tag_name
|
|
117
194
|
current_tags << tag
|
|
118
195
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
196
|
+
zip_asset = find_zip_asset(release)
|
|
197
|
+
fingerprint = release_fingerprint(release, zip_asset)
|
|
198
|
+
cached_files = @deps.delta_state.release_files(repo_key, tag)
|
|
199
|
+
cached_fingerprint = @deps.delta_state.release_fingerprint(repo_key, tag)
|
|
200
|
+
if fingerprint &&
|
|
201
|
+
cached_files.any? &&
|
|
202
|
+
cached_fingerprint == fingerprint &&
|
|
203
|
+
cached_files.all? { |f| File.exist?(File.join(output_dir, f)) }
|
|
204
|
+
publications << build_publication(metadata, cached_files, release,
|
|
205
|
+
repo)
|
|
206
|
+
next
|
|
128
207
|
end
|
|
129
208
|
|
|
130
|
-
zip_asset = find_zip_asset(release)
|
|
131
209
|
next unless zip_asset
|
|
132
210
|
|
|
133
211
|
result = @deps.asset_processor.process(zip_asset.data, metadata_h)
|
|
134
|
-
@deps.delta_state.mark_processed(repo_key, tag,
|
|
135
|
-
result.files.map(&:path)
|
|
212
|
+
@deps.delta_state.mark_processed(repo_key, tag, nil,
|
|
213
|
+
result.files.map(&:path),
|
|
214
|
+
fingerprint: fingerprint)
|
|
136
215
|
publications << build_publication(metadata, result.files.map(&:path),
|
|
137
|
-
|
|
216
|
+
release, repo)
|
|
138
217
|
rescue StandardError => e
|
|
139
218
|
errors << RepoError.new(tag: release.tag_name, message: e.message)
|
|
140
219
|
end
|
|
@@ -150,22 +229,26 @@ module Metanorma
|
|
|
150
229
|
)]
|
|
151
230
|
end
|
|
152
231
|
|
|
153
|
-
def build_publication(metadata, files,
|
|
232
|
+
def build_publication(metadata, files, release, repo)
|
|
154
233
|
metadata.with_files_and_source(files, release, repo)
|
|
155
234
|
end
|
|
156
235
|
|
|
157
|
-
def extract_content_hash(body)
|
|
158
|
-
return nil if body.nil?
|
|
159
|
-
|
|
160
|
-
match = body.match(/^content-hash:([a-f0-9]+)/)
|
|
161
|
-
match ? match[1] : nil
|
|
162
|
-
end
|
|
163
|
-
|
|
164
236
|
def find_zip_asset(release)
|
|
165
237
|
return nil unless release.assets
|
|
166
238
|
|
|
167
239
|
release.assets.find { |a| a.name.end_with?(".zip") }
|
|
168
240
|
end
|
|
241
|
+
|
|
242
|
+
# Identity of a release's CONTENT from listing data alone (no
|
|
243
|
+
# downloads): a force-recreated release gets a new timestamp and
|
|
244
|
+
# asset, so a fingerprint mismatch means the cached extraction is
|
|
245
|
+
# stale even though the tag (and the on-disk files) still exist.
|
|
246
|
+
def release_fingerprint(release, zip_asset)
|
|
247
|
+
return nil if zip_asset.nil?
|
|
248
|
+
|
|
249
|
+
size = zip_asset.respond_to?(:size) ? zip_asset.size : nil
|
|
250
|
+
[release.published_at, zip_asset.name, size].join("|")
|
|
251
|
+
end
|
|
169
252
|
end
|
|
170
253
|
end
|
|
171
254
|
end
|
|
@@ -12,8 +12,6 @@ module Metanorma
|
|
|
12
12
|
class AssetProcessor
|
|
13
13
|
ProcessResult = Struct.new(:files, :channels, keyword_init: true)
|
|
14
14
|
|
|
15
|
-
CANONICALIZE_PATTERN = /-ed\d+(\.\d+)?-/
|
|
16
|
-
|
|
17
15
|
def initialize(output_dir:, routing:, canonicalize: true)
|
|
18
16
|
@output_dir = output_dir
|
|
19
17
|
@routing = routing
|
|
@@ -37,7 +35,9 @@ module Metanorma
|
|
|
37
35
|
dest_path = File.join(@output_dir, relative_path)
|
|
38
36
|
|
|
39
37
|
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
40
|
-
entry.extract(dest_path)
|
|
38
|
+
entry.extract(File.basename(dest_path),
|
|
39
|
+
destination_directory:
|
|
40
|
+
File.dirname(dest_path)) { true }
|
|
41
41
|
|
|
42
42
|
ext = File.extname(file_name).delete_prefix(".")
|
|
43
43
|
files << PublicationFile.new(format: ext, name: file_name,
|
|
@@ -8,12 +8,13 @@ module Metanorma
|
|
|
8
8
|
class ContentHashChangeDetector
|
|
9
9
|
include ChangeDetector
|
|
10
10
|
|
|
11
|
-
def initialize(previous_releases:)
|
|
11
|
+
def initialize(previous_releases:, output_dir: ".")
|
|
12
12
|
@previous_releases = previous_releases
|
|
13
|
+
@output_dir = output_dir
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def detect(publication, tag, force: false)
|
|
16
|
-
current = publication.content_hash
|
|
17
|
+
current = publication.content_hash(from_directory: @output_dir)
|
|
17
18
|
previous = @previous_releases[tag.to_s]
|
|
18
19
|
changed = force || previous.nil? || !current.eql?(previous)
|
|
19
20
|
ChangeResult.new(changed?: changed, current_hash: current,
|
|
@@ -11,6 +11,17 @@ module Metanorma
|
|
|
11
11
|
|
|
12
12
|
class PipelineError < Thor::Error; end
|
|
13
13
|
|
|
14
|
+
desc "version", "Print version"
|
|
15
|
+
def version
|
|
16
|
+
puts "metanorma-release #{VERSION}"
|
|
17
|
+
end
|
|
18
|
+
map %w[--version -v] => :version
|
|
19
|
+
|
|
20
|
+
class_option :verbose, type: :boolean, default: false,
|
|
21
|
+
desc: "Verbose output"
|
|
22
|
+
class_option :quiet, type: :boolean, default: false,
|
|
23
|
+
desc: "Suppress warnings"
|
|
24
|
+
|
|
14
25
|
desc "package", "Package compiled documents"
|
|
15
26
|
option :output_dir, type: :string, default: "_site",
|
|
16
27
|
desc: "Compiled docs directory"
|
|
@@ -21,6 +32,7 @@ module Metanorma
|
|
|
21
32
|
option :config, type: :string, desc: "Config file"
|
|
22
33
|
|
|
23
34
|
def package
|
|
35
|
+
configure_logging
|
|
24
36
|
config = PackageCommand::Config.new(
|
|
25
37
|
output_dir: options[:output_dir],
|
|
26
38
|
dest: options[:dest],
|
|
@@ -49,6 +61,7 @@ module Metanorma
|
|
|
49
61
|
option :config, type: :string, desc: "Config file"
|
|
50
62
|
|
|
51
63
|
def release
|
|
64
|
+
configure_logging
|
|
52
65
|
config = ReleaseCommand::Config.new(
|
|
53
66
|
output_dir: options[:output_dir],
|
|
54
67
|
platform: options[:platform],
|
|
@@ -75,21 +88,24 @@ module Metanorma
|
|
|
75
88
|
option :repos, type: :array, desc: "Explicit repo list"
|
|
76
89
|
option :channels, type: :array, default: [],
|
|
77
90
|
desc: "Filter channels"
|
|
78
|
-
option :output_dir, type: :string,
|
|
79
|
-
desc: "Output directory"
|
|
91
|
+
option :output_dir, type: :string,
|
|
92
|
+
desc: "Output directory (default: config file, or _site/cc)"
|
|
80
93
|
option :file_routing, type: :string,
|
|
81
94
|
desc: "File routing (by-document|flat|by-format)"
|
|
82
95
|
option :cache_dir, type: :string, desc: "Cache directory"
|
|
83
|
-
option :data_dir, type: :string,
|
|
96
|
+
option :data_dir, type: :string,
|
|
97
|
+
desc: "Write flattened documents.json for site generators"
|
|
84
98
|
option :include_drafts, type: :boolean, default: false,
|
|
85
99
|
desc: "Include draft releases"
|
|
86
100
|
option :concurrency, type: :numeric, default: 4
|
|
87
101
|
option :min_documents, type: :numeric, default: 0,
|
|
88
102
|
desc: "Minimum required documents"
|
|
89
103
|
option :token, type: :string, desc: "Platform auth token"
|
|
90
|
-
option :config, type: :string,
|
|
104
|
+
option :config, type: :string,
|
|
105
|
+
desc: "Config file (default: metanorma.aggregate.yml)"
|
|
91
106
|
|
|
92
107
|
def aggregate
|
|
108
|
+
configure_logging
|
|
93
109
|
config = AggregateCommand.build_config(
|
|
94
110
|
source: options[:source],
|
|
95
111
|
organizations: options[:organizations],
|
|
@@ -123,6 +139,14 @@ module Metanorma
|
|
|
123
139
|
|
|
124
140
|
private
|
|
125
141
|
|
|
142
|
+
def configure_logging
|
|
143
|
+
if options[:verbose]
|
|
144
|
+
Metanorma::Release.logger.level = Logger::DEBUG
|
|
145
|
+
elsif options[:quiet]
|
|
146
|
+
Metanorma::Release.logger.level = Logger::ERROR
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
126
150
|
def print_package_result(result, dest)
|
|
127
151
|
released = result.released
|
|
128
152
|
puts "Packaged #{released.length} documents → #{dest}/"
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "base64"
|
|
4
3
|
require "yaml"
|
|
5
4
|
|
|
6
5
|
module Metanorma
|
|
@@ -33,7 +32,6 @@ module Metanorma
|
|
|
33
32
|
site.enrich!
|
|
34
33
|
site.package! if @config.create_zip
|
|
35
34
|
|
|
36
|
-
stamp_primary_identifiers(index)
|
|
37
35
|
result
|
|
38
36
|
end
|
|
39
37
|
|
|
@@ -74,7 +72,7 @@ module Metanorma
|
|
|
74
72
|
topic: cli_options[:topic] || gh["topic"],
|
|
75
73
|
repos: cli_options[:repos] || file_data["repos"],
|
|
76
74
|
channels: cli_options[:channels].any? ? cli_options[:channels] : Array(file_data["channels"]),
|
|
77
|
-
output_dir: cli_options[:output_dir] || file_data["output_dir"],
|
|
75
|
+
output_dir: cli_options[:output_dir] || file_data["output_dir"] || "_site/cc",
|
|
78
76
|
file_routing: cli_options[:file_routing] || file_data["file_routing"] || "by-document",
|
|
79
77
|
cache_dir: cli_options[:cache_dir] || file_data["cache_dir"],
|
|
80
78
|
data_dir: cli_options[:data_dir] || file_data["data_dir"],
|
|
@@ -151,20 +149,6 @@ module Metanorma
|
|
|
151
149
|
output_dir: @config.output_dir,
|
|
152
150
|
)
|
|
153
151
|
end
|
|
154
|
-
|
|
155
|
-
def stamp_primary_identifiers(index)
|
|
156
|
-
index.publications.each do |pub|
|
|
157
|
-
next unless pub.to_h["bibliographic"]
|
|
158
|
-
|
|
159
|
-
ids = pub.to_h.dig("bibliographic", "docidentifier")
|
|
160
|
-
next unless ids&.any?
|
|
161
|
-
|
|
162
|
-
primary = ids.find { |di| di["primary"] == true } || ids.first
|
|
163
|
-
pub.to_h.merge("primary_identifier" => primary["content"])
|
|
164
|
-
end
|
|
165
|
-
rescue LoadError
|
|
166
|
-
warn " (relaton gem not available — bibliography skipped)"
|
|
167
|
-
end
|
|
168
152
|
end
|
|
169
153
|
end
|
|
170
154
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Metanorma
|
|
4
4
|
module Release
|
|
5
5
|
class PackageCommand
|
|
6
|
+
extend ConfigLoader
|
|
7
|
+
|
|
6
8
|
Config = Struct.new(
|
|
7
9
|
:output_dir, :dest, :manifest, :config_source,
|
|
8
10
|
keyword_init: true
|
|
@@ -13,11 +15,15 @@ module Metanorma
|
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def call
|
|
16
|
-
config = load_config
|
|
18
|
+
config = self.class.load_config(
|
|
19
|
+
config_source: @config.config_source,
|
|
20
|
+
manifest: @config.manifest,
|
|
21
|
+
)
|
|
17
22
|
deps = ReleasePipeline::Dependencies.new(
|
|
18
|
-
extractor:
|
|
23
|
+
extractor: RxlExtractor,
|
|
19
24
|
filters: [],
|
|
20
|
-
change_detector: ContentHashChangeDetector.new(previous_releases: {}
|
|
25
|
+
change_detector: ContentHashChangeDetector.new(previous_releases: {},
|
|
26
|
+
output_dir: @config.output_dir),
|
|
21
27
|
packager: ZipPackager.new(output_dir: @config.output_dir),
|
|
22
28
|
publisher: PlatformFactory.build_publisher("null", {}),
|
|
23
29
|
slug_registry: SlugRegistry.from_config(config),
|
|
@@ -28,27 +34,13 @@ module Metanorma
|
|
|
28
34
|
|
|
29
35
|
pipeline_config = ReleasePipeline::Config.new(
|
|
30
36
|
output_dir: @config.output_dir,
|
|
31
|
-
manifest_path: @config.manifest,
|
|
32
37
|
force: false,
|
|
33
38
|
force_replace_patterns: nil,
|
|
34
39
|
concurrency: 4,
|
|
35
|
-
default_visibility: "public",
|
|
36
40
|
)
|
|
37
41
|
|
|
38
42
|
ReleasePipeline.new(deps).run(pipeline_config)
|
|
39
43
|
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
|
|
52
44
|
end
|
|
53
45
|
end
|
|
54
46
|
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "yaml"
|
|
4
|
-
|
|
5
3
|
module Metanorma
|
|
6
4
|
module Release
|
|
7
5
|
class ReleaseCommand
|
|
6
|
+
extend ConfigLoader
|
|
7
|
+
|
|
8
8
|
Config = Struct.new(
|
|
9
9
|
:output_dir, :platform, :manifest, :force,
|
|
10
10
|
:force_replace, :channels, :concurrency, :token, :config_source,
|
|
@@ -16,15 +16,20 @@ module Metanorma
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def call
|
|
19
|
-
config = load_config
|
|
19
|
+
config = self.class.load_config(
|
|
20
|
+
config_source: @config.config_source,
|
|
21
|
+
manifest: @config.manifest,
|
|
22
|
+
)
|
|
20
23
|
options = { token: @config.token }
|
|
21
24
|
publisher = PlatformFactory.build_publisher(@config.platform, options)
|
|
22
25
|
channel_override = Channel.parse_list(@config.channels) if @config.channels
|
|
23
26
|
|
|
27
|
+
previous_releases = build_previous_releases
|
|
24
28
|
deps = ReleasePipeline::Dependencies.new(
|
|
25
|
-
extractor:
|
|
29
|
+
extractor: RxlExtractor,
|
|
26
30
|
filters: [],
|
|
27
|
-
change_detector: ContentHashChangeDetector.new(previous_releases:
|
|
31
|
+
change_detector: ContentHashChangeDetector.new(previous_releases: previous_releases,
|
|
32
|
+
output_dir: @config.output_dir),
|
|
28
33
|
packager: ZipPackager.new(output_dir: @config.output_dir),
|
|
29
34
|
publisher: publisher,
|
|
30
35
|
slug_registry: SlugRegistry.from_config(config),
|
|
@@ -35,11 +40,9 @@ module Metanorma
|
|
|
35
40
|
|
|
36
41
|
pipeline_config = ReleasePipeline::Config.new(
|
|
37
42
|
output_dir: @config.output_dir,
|
|
38
|
-
manifest_path: @config.manifest,
|
|
39
43
|
force: @config.force,
|
|
40
44
|
force_replace_patterns: @config.force_replace && !@config.force_replace.empty? ? @config.force_replace : nil,
|
|
41
45
|
concurrency: @config.concurrency,
|
|
42
|
-
default_visibility: "public",
|
|
43
46
|
)
|
|
44
47
|
|
|
45
48
|
ReleasePipeline.new(deps).run(pipeline_config)
|
|
@@ -47,13 +50,15 @@ module Metanorma
|
|
|
47
50
|
|
|
48
51
|
private
|
|
49
52
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
def build_previous_releases
|
|
54
|
+
pubs = RxlExtractor.discover(@config.output_dir)
|
|
55
|
+
pubs.each_with_object({}) do |pub, map|
|
|
56
|
+
pub.slug
|
|
57
|
+
registry = SlugRegistry.default
|
|
58
|
+
strategy = registry.resolve(SlugStrategy.publisher_from_identifier(pub.identifier))
|
|
59
|
+
tag_info = strategy.compute_tag(pub)
|
|
60
|
+
content_hash = pub.content_hash(from_directory: @config.output_dir)
|
|
61
|
+
map[tag_info[:tag]] = content_hash if content_hash
|
|
57
62
|
end
|
|
58
63
|
end
|
|
59
64
|
end
|
|
@@ -60,13 +60,26 @@ module Metanorma
|
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
#
|
|
63
|
+
# Shared config resolution logic for CLI commands.
|
|
64
|
+
module ConfigLoader
|
|
65
|
+
def load_config(config_source:, manifest:)
|
|
66
|
+
if config_source && File.exist?(config_source)
|
|
67
|
+
Config.from_file(config_source)
|
|
68
|
+
elsif manifest && File.exist?(manifest)
|
|
69
|
+
Config.from_file(manifest)
|
|
70
|
+
else
|
|
71
|
+
Config.defaults
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Single routing entry — matches by any combination of pattern,
|
|
64
77
|
# stage, and doctype. An entry with no criteria matches everything (catch-all).
|
|
65
|
-
DocumentEntry = Struct.new(:pattern, :
|
|
78
|
+
DocumentEntry = Struct.new(:pattern, :stages, :doctypes,
|
|
79
|
+
:channels, keyword_init: true) do
|
|
66
80
|
def initialize(data)
|
|
67
81
|
super(
|
|
68
82
|
pattern: data["pattern"],
|
|
69
|
-
source: data["source"],
|
|
70
83
|
stages: Array(data["stage"]).map(&:to_s),
|
|
71
84
|
doctypes: Array(data["doctype"]).map(&:to_s),
|
|
72
85
|
channels: Array(data["channels"]).map(&:to_s),
|
|
@@ -80,10 +93,6 @@ module Metanorma
|
|
|
80
93
|
return false
|
|
81
94
|
end
|
|
82
95
|
|
|
83
|
-
if source && !(publication.source_path&.end_with?(source) || false)
|
|
84
|
-
return false
|
|
85
|
-
end
|
|
86
|
-
|
|
87
96
|
if !stages.empty? && !stages.include?(publication.stage.to_s)
|
|
88
97
|
return false
|
|
89
98
|
end
|
|
@@ -29,6 +29,10 @@ module Metanorma
|
|
|
29
29
|
raise NotImplementedError, "#{self.class} must implement #release_files"
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
def release_fingerprint(repo_key, tag)
|
|
33
|
+
raise NotImplementedError, "#{self.class} must implement #release_fingerprint"
|
|
34
|
+
end
|
|
35
|
+
|
|
32
36
|
def mark_processed(repo_key, tag, content_hash, files)
|
|
33
37
|
raise NotImplementedError,
|
|
34
38
|
"#{self.class} must implement #mark_processed"
|
|
@@ -85,11 +89,20 @@ module Metanorma
|
|
|
85
89
|
releases[tag]["files"] || []
|
|
86
90
|
end
|
|
87
91
|
|
|
88
|
-
def
|
|
92
|
+
def release_fingerprint(repo_key, tag)
|
|
93
|
+
releases = repo_state(repo_key)["releases"]
|
|
94
|
+
return nil unless releases.key?(tag)
|
|
95
|
+
|
|
96
|
+
releases[tag]["fingerprint"]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def mark_processed(repo_key, tag, content_hash, files,
|
|
100
|
+
fingerprint: nil)
|
|
89
101
|
repo = ensure_repo(repo_key)
|
|
90
102
|
repo["releases"][tag] = {
|
|
91
103
|
"content_hash" => content_hash.to_s,
|
|
92
104
|
"files" => files,
|
|
105
|
+
"fingerprint" => fingerprint,
|
|
93
106
|
}
|
|
94
107
|
end
|
|
95
108
|
|
|
@@ -138,6 +151,7 @@ module Metanorma
|
|
|
138
151
|
def set_etag(_repo_key, _etag) = nil
|
|
139
152
|
def processed?(*_args) = false
|
|
140
153
|
def release_files(*_args) = []
|
|
154
|
+
def release_fingerprint(*_args) = nil
|
|
141
155
|
def mark_processed(*_args); end
|
|
142
156
|
def cleanup_stale(*_args) = 0
|
|
143
157
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Release
|
|
5
|
+
module DependencyValidation
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def validate_interface!(obj, mod, name)
|
|
9
|
+
return if obj.is_a?(mod) || begin
|
|
10
|
+
obj.class.ancestors.include?(mod)
|
|
11
|
+
rescue StandardError
|
|
12
|
+
false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
raise ArgumentError, "#{name} must include #{mod}, got #{obj.class}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|