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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d042d82fb6c1175f03ccb4534e040db56ca33378c116e316937a873c275c2219
4
- data.tar.gz: cefa7d75ac920f733ad39c5161557baf40533f21660b2341d0a95a76a678e32a
3
+ metadata.gz: edbfe878db6e725d2335b5361abd3ab85a5c9e01bca54e4465235ea66c14b62b
4
+ data.tar.gz: f14879f8a6e09373db94081064af2fd96e7ca1e650df42bbdabbeea98af77f1c
5
5
  SHA512:
6
- metadata.gz: ae3fe6c644f91c61fb4e8192ff312a42d94fe0c52e08701d5d54edb73de7048837ba06e3e77757ff96eba3a0a9ac93222c18a8aec7b8bbd23abe966f18a5b1be
7
- data.tar.gz: 052b5bdb91acdaaa8a98248f734f534408cfea074ca4a819fc2bad003a1f076b26a9ac25819df2e0949b48b681192772c2efa81264dafe40d686468f9a24f2d1
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::Publication.discover("_site")
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
- - stages: ["20", "30"]
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
- FetchResult = Struct.new(:releases, :etag, :unchanged?, keyword_init: true)
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
- if cached_files.any? && cached_files.all? do |f|
155
- File.exist?(File.join(output_dir, f))
156
- end
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) { true }
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, default: "_site/cc",
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 mark_processed(repo_key, tag, content_hash, files)
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, Zip::File::CREATE) do |zipfile|
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Metanorma
4
4
  module Release
5
- VERSION = "0.2.25"
5
+ VERSION = "0.2.26"
6
6
  end
7
7
  end
@@ -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, Zip::File::CREATE) do |zipfile|
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.25
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-01 00:00:00.000000000 Z
11
+ date: 2026-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: relaton-bib
14
+ name: relaton
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.1'
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: '2.1'
26
+ version: 3.1.1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: thor
29
29
  requirement: !ruby/object:Gem::Requirement