metanorma-release 0.2.25 → 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/README.adoc +14 -2
- data/lib/metanorma/release/aggregation_pipeline.rb +66 -8
- data/lib/metanorma/release/asset_processor.rb +3 -1
- data/lib/metanorma/release/cli.rb +2 -2
- data/lib/metanorma/release/commands/aggregate.rb +1 -1
- data/lib/metanorma/release/delta_state.rb +15 -1
- data/lib/metanorma/release/platform/github/release_fetcher.rb +6 -4
- data/lib/metanorma/release/site.rb +1 -1
- data/lib/metanorma/release/version.rb +1 -1
- data/lib/metanorma/release/zip_packager.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: edbfe878db6e725d2335b5361abd3ab85a5c9e01bca54e4465235ea66c14b62b
|
|
4
|
+
data.tar.gz: f14879f8a6e09373db94081064af2fd96e7ca1e650df42bbdabbeea98af77f1c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e44a4bd8f2d81326a395779f1df4cf4626edd9f8ef15bce4f528b1f9d94d4c5d7c92d1913c80258d80dc386894869375b4cd4e2c2198a7bf5efddfb80b7dbc08
|
|
7
|
+
data.tar.gz: f3748555ca6418d71c8c045c35d81d3499fafa215a97c1a7361fc4dac9db4fc2e36c105e5fa9d7cbce31e830aaad4a5ac9f10f951705542d9f7e74ece3525141
|
data/README.adoc
CHANGED
|
@@ -109,7 +109,7 @@ metanorma-release aggregate
|
|
|
109
109
|
[source,ruby]
|
|
110
110
|
----
|
|
111
111
|
# Discover publications from compiled RXL files
|
|
112
|
-
publications = Metanorma::Release::
|
|
112
|
+
publications = Metanorma::Release::RxlExtractor.discover("_site")
|
|
113
113
|
|
|
114
114
|
pub = publications.first
|
|
115
115
|
pub.identifier # => "CC 18011:2018"
|
|
@@ -243,7 +243,7 @@ You can also route by stage or doctype instead of pattern:
|
|
|
243
243
|
[source,yaml]
|
|
244
244
|
----
|
|
245
245
|
documents:
|
|
246
|
-
-
|
|
246
|
+
- stage: ["20", "30"]
|
|
247
247
|
channels: [internal]
|
|
248
248
|
- doctypes: [standard, specification]
|
|
249
249
|
channels: [public/standards]
|
|
@@ -342,6 +342,18 @@ github:
|
|
|
342
342
|
|
|
343
343
|
CLI flags override config file values.
|
|
344
344
|
|
|
345
|
+
=== Global options
|
|
346
|
+
|
|
347
|
+
All commands accept these options:
|
|
348
|
+
|
|
349
|
+
[cols="1m,3",options="header"]
|
|
350
|
+
|===
|
|
351
|
+
|Option |Description
|
|
352
|
+
|`--verbose` |Enable debug-level logging
|
|
353
|
+
|`--quiet` |Suppress warnings (only errors shown)
|
|
354
|
+
|`--help` |Show command help
|
|
355
|
+
|===
|
|
356
|
+
|
|
345
357
|
== Concepts
|
|
346
358
|
|
|
347
359
|
=== Channels
|
|
@@ -2,10 +2,21 @@
|
|
|
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
21
|
class AggregationPipeline
|
|
11
22
|
Dependencies = Struct.new(
|
|
@@ -39,9 +50,14 @@ module Metanorma
|
|
|
39
50
|
|
|
40
51
|
Result = Struct.new(
|
|
41
52
|
:publications, :repo_count, :channels_found,
|
|
42
|
-
:report, :failed_repos,
|
|
53
|
+
:report, :failed_repos, :collisions,
|
|
43
54
|
keyword_init: true
|
|
44
|
-
)
|
|
55
|
+
) do
|
|
56
|
+
def initialize(publications:, repo_count:, channels_found:, report:,
|
|
57
|
+
failed_repos:, collisions: [])
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
end
|
|
45
61
|
|
|
46
62
|
def initialize(deps)
|
|
47
63
|
@deps = deps
|
|
@@ -67,9 +83,25 @@ module Metanorma
|
|
|
67
83
|
channels_found: publications.flat_map(&:channels).uniq.sort,
|
|
68
84
|
report: reports,
|
|
69
85
|
failed_repos: failed_repos,
|
|
86
|
+
collisions: detect_collisions(publications),
|
|
70
87
|
)
|
|
71
88
|
end
|
|
72
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
|
+
|
|
73
105
|
private
|
|
74
106
|
|
|
75
107
|
def run_sequential(repos, output_dir, config)
|
|
@@ -130,6 +162,17 @@ module Metanorma
|
|
|
130
162
|
etag = @deps.delta_state.etag(repo_key)
|
|
131
163
|
fetch_result = @deps.fetcher.fetch(repo, etag: etag)
|
|
132
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
|
+
|
|
133
176
|
if fetch_result.unchanged?
|
|
134
177
|
return [], RepoReport.new(releases: 0, included: 0, skipped: 0,
|
|
135
178
|
reason: "etag unchanged", errors: [])
|
|
@@ -150,21 +193,25 @@ module Metanorma
|
|
|
150
193
|
tag = release.tag_name
|
|
151
194
|
current_tags << tag
|
|
152
195
|
|
|
196
|
+
zip_asset = find_zip_asset(release)
|
|
197
|
+
fingerprint = release_fingerprint(release, zip_asset)
|
|
153
198
|
cached_files = @deps.delta_state.release_files(repo_key, tag)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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)) }
|
|
157
204
|
publications << build_publication(metadata, cached_files, release,
|
|
158
205
|
repo)
|
|
159
206
|
next
|
|
160
207
|
end
|
|
161
208
|
|
|
162
|
-
zip_asset = find_zip_asset(release)
|
|
163
209
|
next unless zip_asset
|
|
164
210
|
|
|
165
211
|
result = @deps.asset_processor.process(zip_asset.data, metadata_h)
|
|
166
212
|
@deps.delta_state.mark_processed(repo_key, tag, nil,
|
|
167
|
-
result.files.map(&:path)
|
|
213
|
+
result.files.map(&:path),
|
|
214
|
+
fingerprint: fingerprint)
|
|
168
215
|
publications << build_publication(metadata, result.files.map(&:path),
|
|
169
216
|
release, repo)
|
|
170
217
|
rescue StandardError => e
|
|
@@ -191,6 +238,17 @@ module Metanorma
|
|
|
191
238
|
|
|
192
239
|
release.assets.find { |a| a.name.end_with?(".zip") }
|
|
193
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
|
|
194
252
|
end
|
|
195
253
|
end
|
|
196
254
|
end
|
|
@@ -35,7 +35,9 @@ module Metanorma
|
|
|
35
35
|
dest_path = File.join(@output_dir, relative_path)
|
|
36
36
|
|
|
37
37
|
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
38
|
-
entry.extract(dest_path)
|
|
38
|
+
entry.extract(File.basename(dest_path),
|
|
39
|
+
destination_directory:
|
|
40
|
+
File.dirname(dest_path)) { true }
|
|
39
41
|
|
|
40
42
|
ext = File.extname(file_name).delete_prefix(".")
|
|
41
43
|
files << PublicationFile.new(format: ext, name: file_name,
|
|
@@ -88,8 +88,8 @@ module Metanorma
|
|
|
88
88
|
option :repos, type: :array, desc: "Explicit repo list"
|
|
89
89
|
option :channels, type: :array, default: [],
|
|
90
90
|
desc: "Filter channels"
|
|
91
|
-
option :output_dir, type: :string,
|
|
92
|
-
desc: "Output directory"
|
|
91
|
+
option :output_dir, type: :string,
|
|
92
|
+
desc: "Output directory (default: config file, or _site/cc)"
|
|
93
93
|
option :file_routing, type: :string,
|
|
94
94
|
desc: "File routing (by-document|flat|by-format)"
|
|
95
95
|
option :cache_dir, type: :string, desc: "Cache directory"
|
|
@@ -72,7 +72,7 @@ module Metanorma
|
|
|
72
72
|
topic: cli_options[:topic] || gh["topic"],
|
|
73
73
|
repos: cli_options[:repos] || file_data["repos"],
|
|
74
74
|
channels: cli_options[:channels].any? ? cli_options[:channels] : Array(file_data["channels"]),
|
|
75
|
-
output_dir: cli_options[:output_dir] || file_data["output_dir"],
|
|
75
|
+
output_dir: cli_options[:output_dir] || file_data["output_dir"] || "_site/cc",
|
|
76
76
|
file_routing: cli_options[:file_routing] || file_data["file_routing"] || "by-document",
|
|
77
77
|
cache_dir: cli_options[:cache_dir] || file_data["cache_dir"],
|
|
78
78
|
data_dir: cli_options[:data_dir] || file_data["data_dir"],
|
|
@@ -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
|
|
@@ -15,14 +15,16 @@ module Metanorma
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def fetch(repo, etag: nil)
|
|
18
|
-
releases = paginate_releases(repo.to_s)
|
|
18
|
+
releases, failed = paginate_releases(repo.to_s)
|
|
19
19
|
parsed = releases.map { |r| parse_release(r) }
|
|
20
20
|
FetchResult.new(releases: parsed, etag: "etag-#{repo}",
|
|
21
|
-
unchanged?: false)
|
|
21
|
+
unchanged?: false, failed?: failed)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
|
+
# Returns [releases, failed] — failed is true when the listing
|
|
27
|
+
# errored, which callers must not treat as an empty release set.
|
|
26
28
|
def paginate_releases(repo_slug)
|
|
27
29
|
all = []
|
|
28
30
|
page = 1
|
|
@@ -35,10 +37,10 @@ module Metanorma
|
|
|
35
37
|
|
|
36
38
|
page += 1
|
|
37
39
|
end
|
|
38
|
-
all
|
|
40
|
+
[all, false]
|
|
39
41
|
rescue StandardError => e
|
|
40
42
|
Metanorma::Release.logger.warn "Failed to fetch releases for #{repo_slug}: #{e.message}"
|
|
41
|
-
[]
|
|
43
|
+
[[], true]
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def parse_release(r)
|
|
@@ -33,7 +33,7 @@ module Metanorma
|
|
|
33
33
|
require "zip"
|
|
34
34
|
|
|
35
35
|
path = zip_path || "#{output_dir}.zip"
|
|
36
|
-
Zip::File.open(path,
|
|
36
|
+
Zip::File.open(path, create: true) do |zipfile|
|
|
37
37
|
Dir.glob("#{output_dir}/**/*").each do |file|
|
|
38
38
|
next if File.directory?(file)
|
|
39
39
|
|
|
@@ -35,7 +35,7 @@ module Metanorma
|
|
|
35
35
|
|
|
36
36
|
zip_path = File.join(dir, "#{canonical_base}.zip")
|
|
37
37
|
FileUtils.rm_f(zip_path)
|
|
38
|
-
Zip::File.open(zip_path,
|
|
38
|
+
Zip::File.open(zip_path, create: true) do |zipfile|
|
|
39
39
|
files.each do |file|
|
|
40
40
|
ext = File.extname(file)
|
|
41
41
|
entry_name = "#{canonical_base}#{ext}"
|
metadata
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: metanorma-release
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.26
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06
|
|
11
|
+
date: 2026-11-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: relaton
|
|
14
|
+
name: relaton
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
19
|
+
version: 3.1.1
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
26
|
+
version: 3.1.1
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: thor
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|