metanorma-release 0.2.4 → 0.2.6

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: 83c0192b0bb05996d578d29961debb99d4600e0e1a178f259b1f4be4b8bbf065
4
- data.tar.gz: fe1669b07a72cceb289350d042e9a5eec6eea71ad77b3f5ae6699f30977a3ad3
3
+ metadata.gz: fd5a3df2d9c64ed9d7d70d8f4c3fd3646d8c932a4cbffc39c07490e0276e37d9
4
+ data.tar.gz: f688bc55c1dff3751cdc1903e420e64c7503f7716710655c123e034dd0a3a3fb
5
5
  SHA512:
6
- metadata.gz: 4bf35ae312524e91297d084901835d75255a9af7dd5bff78f4fcd45eea0813ba3ac574eca6529ebc31b21624f5fd28eeefbc4c328fe42b4121a1211d538a0856
7
- data.tar.gz: c430938830a22af1df1a904253fd26a6710108b0bb544010f903ac7b5df141d2f6780961d4924579782a77fc6b19aa8dfc23fedebc8ccbad001fd6d2acf1c8e7
6
+ metadata.gz: 8127051648875b3ea36b988542c9ae82954bf68caf4ad91b785543d8ff6518ef95e1caa7d55334a777c22567806fbfa6cd32014812e6b2885388f42997f881d1
7
+ data.tar.gz: 77c9e3f867de101f3e962d674c8ddcabc630b9ba45557afe4c6338af0669a572dee37432b8231887cde7964f143817ea4ee1b50c9549bf2875dcdc76542152fe
data/README.adoc CHANGED
@@ -133,6 +133,8 @@ metanorma-release release [options]
133
133
  === `metanorma-release aggregate`
134
134
 
135
135
  Aggregate published releases from multiple repositories into a unified file tree.
136
+ Reads config from `metanorma.aggregate.yml` if present (auto-detected).
137
+ Idempotent: uses cached delta state (`.cache/aggregate/`) to skip unchanged repos.
136
138
 
137
139
  [source,sh]
138
140
  ----
@@ -142,21 +144,40 @@ metanorma-release aggregate [options]
142
144
  [cols="1m,3",options="header"]
143
145
  |===
144
146
  |Option |Description
147
+ |`--config FILE` |Config file (default: `metanorma.aggregate.yml`)
145
148
  |`--source SOURCE` |Discovery source: `github`, `local:PATH` (default: `github`)
146
- |`--organizations ORGS` |Organization list
149
+ |`--organizations ORGS` |Organization list (overrides config)
147
150
  |`--topic TOPIC` |Repository topic filter (default: `metanorma-release`)
148
151
  |`--repos REPOS` |Explicit repo list
149
152
  |`--channels CHANS` |Filter channels
150
153
  |`--stages STAGES` |Filter stages
151
154
  |`--output-dir DIR` |Output directory (default: `_site/cc`)
152
155
  |`--file-routing MODE` |File layout: `by-document`, `flat`, `by-format` (default: `by-document`)
153
- |`--cache-dir DIR` |Cache directory for delta state
154
156
  |`--[no-]include-drafts` |Include draft releases
155
157
  |`--concurrency N` |Parallel repos (default: 4)
156
158
  |`--min-documents N` |Fail if fewer documents found (default: 0)
157
- |`--token TOKEN` |Platform auth token
159
+ |`--token TOKEN` |Platform auth token (falls back to `GITHUB_TOKEN` env)
158
160
  |===
159
161
 
162
+ ==== Aggregate config file
163
+
164
+ Create `metanorma.aggregate.yml` in your project root:
165
+
166
+ [source,yaml]
167
+ ----
168
+ source: github
169
+ output_dir: _site/cc
170
+ file_routing: flat
171
+ cache_dir: .cache/aggregate
172
+
173
+ github:
174
+ organizations:
175
+ - MyOrg
176
+ topic: metanorma-release
177
+ ----
178
+
179
+ CLI flags override config file values. Cache is always enabled (defaults to `.cache/aggregate/`).
180
+
160
181
  == Concepts
161
182
 
162
183
  === Publication
@@ -82,15 +82,17 @@ module Metanorma
82
82
  option :file_routing, type: :string, default: "by-document",
83
83
  desc: "File routing (by-document|flat|by-format)"
84
84
  option :cache_dir, type: :string, desc: "Cache directory"
85
+ option :data_dir, type: :string, desc: "Write flattened documents.json for site generators"
85
86
  option :include_drafts, type: :boolean, default: false,
86
87
  desc: "Include draft releases"
87
88
  option :concurrency, type: :numeric, default: 4
88
89
  option :min_documents, type: :numeric, default: 0,
89
90
  desc: "Minimum required documents"
90
91
  option :token, type: :string, desc: "Platform auth token"
92
+ option :config, type: :string, desc: "Config file (default: metanorma.aggregate.yml)"
91
93
 
92
94
  def aggregate
93
- config = AggregateCommand::Config.new(
95
+ config = AggregateCommand.build_config(
94
96
  source: options[:source],
95
97
  organizations: options[:organizations],
96
98
  topic: options[:topic],
@@ -100,18 +102,20 @@ module Metanorma
100
102
  output_dir: options[:output_dir],
101
103
  file_routing: options[:file_routing],
102
104
  cache_dir: options[:cache_dir],
105
+ data_dir: options[:data_dir],
103
106
  include_drafts: options[:include_drafts],
104
107
  concurrency: options[:concurrency],
105
108
  min_documents: options[:min_documents],
106
109
  token: options[:token],
107
110
  create_zip: nil,
111
+ config: options[:config],
108
112
  )
109
113
  result = AggregateCommand.new(config).call
110
114
  print_aggregate_result(result)
111
115
 
112
- if options[:min_documents].positive? && result.publications.length < options[:min_documents]
116
+ if config.min_documents.positive? && result.publications.length < config.min_documents
113
117
  raise PipelineError,
114
- "Found #{result.publications.length} documents, minimum is #{options[:min_documents]}"
118
+ "Found #{result.publications.length} documents, minimum is #{config.min_documents}"
115
119
  end
116
120
 
117
121
  unless result.failed_repos.empty?
@@ -1,15 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "yaml"
4
+
3
5
  module Metanorma
4
6
  module Release
5
7
  class AggregateCommand
6
8
  Config = Struct.new(
7
9
  :source, :organizations, :topic, :repos, :repo_pattern, :local_path,
8
10
  :channels, :stages, :output_dir, :file_routing, :cache_dir,
9
- :include_drafts, :concurrency, :min_documents, :token, :create_zip,
11
+ :data_dir, :include_drafts, :concurrency, :min_documents, :token,
12
+ :create_zip,
10
13
  keyword_init: true
11
14
  )
12
15
 
16
+ DEFAULT_CONFIG_FILE = "metanorma.aggregate.yml"
17
+ DEFAULT_CACHE_DIR = ".cache/aggregate"
18
+
13
19
  def initialize(config)
14
20
  @config = config
15
21
  end
@@ -19,7 +25,8 @@ module Metanorma
19
25
  return result unless result.publications.any?
20
26
 
21
27
  index = build_index(result)
22
- site = Site.new(index: index, output_dir: @config.output_dir)
28
+ site = Site.new(index: index, output_dir: @config.output_dir,
29
+ data_dir: @config.data_dir)
23
30
  site.write!
24
31
  site.enrich!
25
32
  site.package! if @config.create_zip
@@ -28,6 +35,56 @@ module Metanorma
28
35
  result
29
36
  end
30
37
 
38
+ def self.build_config(cli_options)
39
+ file_data = load_config_file(cli_options[:config])
40
+ merged = merge_config(file_data, cli_options)
41
+ Config.new(
42
+ source: merged[:source],
43
+ organizations: merged[:organizations],
44
+ topic: merged[:topic],
45
+ repos: merged[:repos],
46
+ channels: merged[:channels],
47
+ stages: merged[:stages],
48
+ output_dir: merged[:output_dir],
49
+ file_routing: merged[:file_routing],
50
+ cache_dir: merged[:cache_dir] || DEFAULT_CACHE_DIR,
51
+ data_dir: merged[:data_dir],
52
+ include_drafts: merged[:include_drafts],
53
+ concurrency: merged[:concurrency],
54
+ min_documents: merged[:min_documents],
55
+ token: merged[:token],
56
+ create_zip: merged[:create_zip],
57
+ )
58
+ end
59
+
60
+ def self.load_config_file(path)
61
+ path ||= DEFAULT_CONFIG_FILE
62
+ return {} unless File.exist?(path)
63
+
64
+ YAML.safe_load_file(path, permitted_classes: [Symbol]) || {}
65
+ end
66
+
67
+ def self.merge_config(file_data, cli_options)
68
+ gh = file_data["github"] || {}
69
+ {
70
+ source: cli_options[:source] || file_data["source"],
71
+ organizations: cli_options[:organizations].any? ? cli_options[:organizations] : Array(gh["organizations"]),
72
+ topic: cli_options[:topic] || gh["topic"],
73
+ repos: cli_options[:repos] || file_data["repos"],
74
+ channels: cli_options[:channels].any? ? cli_options[:channels] : Array(file_data["channels"]),
75
+ stages: cli_options[:stages].any? ? cli_options[:stages] : Array(file_data["stages"]),
76
+ output_dir: cli_options[:output_dir] || file_data["output_dir"],
77
+ file_routing: cli_options[:file_routing] || file_data["file_routing"],
78
+ cache_dir: cli_options[:cache_dir] || file_data["cache_dir"],
79
+ data_dir: cli_options[:data_dir] || file_data["data_dir"],
80
+ include_drafts: cli_options[:include_drafts] || file_data["include_drafts"],
81
+ concurrency: cli_options[:concurrency] || file_data["concurrency"],
82
+ min_documents: cli_options[:min_documents] || file_data["min_documents"],
83
+ token: cli_options[:token],
84
+ create_zip: cli_options[:create_zip],
85
+ }
86
+ end
87
+
31
88
  private
32
89
 
33
90
  def run_aggregation
@@ -64,7 +64,8 @@ module Metanorma
64
64
 
65
65
  def self.build_github_client(token)
66
66
  require "octokit"
67
- token ? Octokit::Client.new(access_token: token) : Octokit::Client.new
67
+ access_token = token || ENV.fetch("GITHUB_TOKEN", nil)
68
+ access_token ? Octokit::Client.new(access_token: access_token) : Octokit::Client.new
68
69
  end
69
70
 
70
71
  def self.register_publisher(name, factory)
@@ -9,9 +9,10 @@ module Metanorma
9
9
  class Site
10
10
  attr_reader :index, :output_dir
11
11
 
12
- def initialize(index:, output_dir:)
12
+ def initialize(index:, output_dir:, data_dir: nil)
13
13
  @index = index
14
14
  @output_dir = output_dir
15
+ @data_dir = data_dir
15
16
  end
16
17
 
17
18
  def write!
@@ -22,22 +23,49 @@ module Metanorma
22
23
  def enrich!
23
24
  return if index.empty?
24
25
 
25
- documents = index.publications.map do |pub|
26
- rxl_file = pub.files.find { |f| f.format == "rxl" }
27
- next pub.to_h unless rxl_file
26
+ documents = enrich_documents
27
+ write_relaton_index(documents)
28
+ write_data_file(documents) if @data_dir
29
+ end
30
+
31
+ def package!(zip_path: nil)
32
+ require "zip"
28
33
 
29
- rxl_path = File.join(output_dir, rxl_file.path)
30
- next pub.to_h unless File.exist?(rxl_path)
34
+ path = zip_path || "#{output_dir}.zip"
35
+ Zip::File.open(path, Zip::File::CREATE) do |zipfile|
36
+ Dir.glob("#{output_dir}/**/*").each do |file|
37
+ next if File.directory?(file)
31
38
 
32
- bib = Relaton::Bib::Item.from_xml(File.read(rxl_path))
33
- enriched = pub.to_h
34
- enriched["bibliographic"] = bib.to_h
35
- enriched
39
+ entry_name = file.sub("#{File.dirname(output_dir)}/", "")
40
+ zipfile.add(entry_name, file)
41
+ end
42
+ end
43
+ path
44
+ end
45
+
46
+ private
47
+
48
+ def enrich_documents
49
+ index.publications.map do |pub|
50
+ enrich_publication(pub)
36
51
  rescue StandardError => e
37
52
  warn " Skip #{pub.identifier}: #{e.message}"
38
53
  pub.to_h
39
54
  end
55
+ end
56
+
57
+ def enrich_publication(pub)
58
+ rxl_file = pub.files.find { |f| f.format == "rxl" }
59
+ return pub.to_h unless rxl_file
40
60
 
61
+ rxl_path = File.join(output_dir, rxl_file.path)
62
+ return pub.to_h unless File.exist?(rxl_path)
63
+
64
+ bib = Relaton::Bib::Item.from_xml(File.read(rxl_path))
65
+ pub.to_h.merge("bibliographic" => bib.to_h)
66
+ end
67
+
68
+ def write_relaton_index(documents)
41
69
  dest = File.join(output_dir, "relaton")
42
70
  FileUtils.mkdir_p(dest)
43
71
  index_data = { "root" => { "title" => "Document Registry",
@@ -47,19 +75,59 @@ module Metanorma
47
75
  File.write(File.join(dest, "index.yaml"), YAML.dump(index_data))
48
76
  end
49
77
 
50
- def package!(zip_path: nil)
51
- require "zip"
78
+ def write_data_file(documents)
79
+ FileUtils.mkdir_p(@data_dir)
80
+ items = documents.compact.map { |doc| flatten_for_site(doc) }
81
+ File.write(File.join(@data_dir, "documents.json"),
82
+ JSON.pretty_generate({ "items" => items }))
83
+ end
52
84
 
53
- path = zip_path || "#{output_dir}.zip"
54
- Zip::File.open(path, Zip::File::CREATE) do |zipfile|
55
- Dir.glob("#{output_dir}/**/*").each do |file|
56
- next if File.directory?(file)
85
+ def flatten_for_site(doc)
86
+ bib = doc["bibliographic"] || {}
87
+ doc_id = resolve_doc_id(bib, doc)
88
+ {
89
+ "slug" => doc["id"],
90
+ "id" => doc_id,
91
+ "title" => doc["title"].to_s,
92
+ "abstract" => extract_abstract(bib),
93
+ "stage" => (doc["stage"] || "published").to_s.downcase,
94
+ "doctype" => extract_doctype(bib) || doc.fetch("doctype", ""),
95
+ "edition" => doc["edition"],
96
+ "date" => extract_date(doc),
97
+ "channels" => doc["channels"] || [],
98
+ "formats" => doc["formats"] || [],
99
+ "files" => doc["files"] || [],
100
+ }
101
+ end
57
102
 
58
- entry_name = file.sub("#{File.dirname(output_dir)}/", "")
59
- zipfile.add(entry_name, file)
60
- end
61
- end
62
- path
103
+ def resolve_doc_id(bib, doc)
104
+ extract_primary_id(bib) || doc["identifier"] || doc["id"]
105
+ end
106
+
107
+ def extract_primary_id(bib)
108
+ ids = bib["docidentifier"]
109
+ return nil unless ids&.any?
110
+
111
+ primary = ids.find { |di| di["primary"] == true } || ids.first
112
+ primary["content"]
113
+ end
114
+
115
+ def extract_doctype(bib)
116
+ bib.dig("ext", "doctype", "content")
117
+ end
118
+
119
+ def extract_abstract(bib)
120
+ abstracts = bib["abstract"]
121
+ return nil unless abstracts&.any?
122
+
123
+ abstracts.first["content"]
124
+ end
125
+
126
+ def extract_date(doc)
127
+ release_date = doc.dig("source", "releaseDate")
128
+ return nil unless release_date
129
+
130
+ release_date.to_s.split(/[T ]/).first
63
131
  end
64
132
  end
65
133
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Metanorma
4
4
  module Release
5
- VERSION = "0.2.4"
5
+ VERSION = "0.2.6"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metanorma-release
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.6
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-05-17 00:00:00.000000000 Z
11
+ date: 2026-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: relaton-bib