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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +21 -319
  3. data/README.adoc +319 -92
  4. data/lib/metanorma/release/aggregation_pipeline.rb +128 -45
  5. data/lib/metanorma/release/asset_processor.rb +3 -3
  6. data/lib/metanorma/release/cache_store.rb +1 -0
  7. data/lib/metanorma/release/change_detector.rb +3 -2
  8. data/lib/metanorma/release/cli.rb +28 -4
  9. data/lib/metanorma/release/commands/aggregate.rb +1 -17
  10. data/lib/metanorma/release/commands/package.rb +9 -17
  11. data/lib/metanorma/release/commands/release_command.rb +19 -14
  12. data/lib/metanorma/release/config.rb +16 -7
  13. data/lib/metanorma/release/delta_state.rb +15 -1
  14. data/lib/metanorma/release/dependency_validation.rb +19 -0
  15. data/lib/metanorma/release/document_flattener.rb +173 -0
  16. data/lib/metanorma/release/index.rb +1 -1
  17. data/lib/metanorma/release/interfaces.rb +12 -0
  18. data/lib/metanorma/release/platform/github/manifest_reader.rb +3 -1
  19. data/lib/metanorma/release/platform/github/release_fetcher.rb +10 -14
  20. data/lib/metanorma/release/platform/github/topic_discoverer.rb +1 -1
  21. data/lib/metanorma/release/platform/github.rb +0 -4
  22. data/lib/metanorma/release/platform/local/fetcher.rb +5 -17
  23. data/lib/metanorma/release/platform/null/manifest_reader.rb +15 -0
  24. data/lib/metanorma/release/platform/null/publisher.rb +1 -1
  25. data/lib/metanorma/release/platform/null.rb +2 -0
  26. data/lib/metanorma/release/platform/static_discoverer.rb +19 -0
  27. data/lib/metanorma/release/platform.rb +1 -0
  28. data/lib/metanorma/release/platform_factory.rb +6 -21
  29. data/lib/metanorma/release/publication.rb +23 -161
  30. data/lib/metanorma/release/publication_serializer.rb +59 -0
  31. data/lib/metanorma/release/release_pipeline.rb +7 -15
  32. data/lib/metanorma/release/rxl_extractor.rb +106 -0
  33. data/lib/metanorma/release/site.rb +5 -165
  34. data/lib/metanorma/release/slug_strategy.rb +30 -15
  35. data/lib/metanorma/release/version.rb +1 -1
  36. data/lib/metanorma/release/zip_packager.rb +1 -1
  37. data/lib/metanorma/release.rb +36 -19
  38. metadata +11 -5
data/README.adoc CHANGED
@@ -8,18 +8,25 @@ toc::[]
8
8
 
9
9
  == Overview
10
10
 
11
- `metanorma-release` manages the full release lifecycle of Metanorma documents through three actors:
11
+ `metanorma-release` manages the full release lifecycle of Metanorma documents through two config files:
12
12
 
13
- **Doc repo** (producer)::
14
- Discover compiled documents -> extract metadata from RXL -> detect changes -> package as zip -> release to a platform (GitHub Releases, local filesystem).
13
+ **Per-repo** (`metanorma.release.yml`)::
14
+ Defines routing rules that map documents to channel labels based on slug pattern, stage, or doctype.
15
+ Read by the `release` command to tag releases with channel metadata.
15
16
 
16
- **Org/publisher** (governor)::
17
- Define channels and routing rules via config. Map document metadata (stage, doctype) to channel labels that control visibility and distribution.
17
+ **Per-site** (`metanorma.aggregate.yml`)::
18
+ Defines discovery (which repos to aggregate), channel subscription (which channels to include), display categories, and output layout.
19
+ Read by the `aggregate` command to build a file tree + `index.json` for any site generator.
18
20
 
19
- **Aggregator** (consumer)::
20
- Discover repositories -> fetch published releases -> filter by channel and stage -> extract zip assets -> generate `index.json` with Relaton enrichment and a file tree for any site generator.
21
+ The output is platform-agnostic: a directory containing `index.json` and a tree of document files (HTML, PDF, XML, RXL).
22
+ Any site generator (Jekyll, Hugo, Vite) consumes that output independently.
21
23
 
22
- The output is platform-agnostic: a directory containing `index.json` and a tree of document files. Any site generator (Jekyll, Hugo, Vite) consumes that output independently.
24
+ === How it works
25
+
26
+ . **Compile** — Metanorma compiles `.adoc` sources to HTML, PDF, XML, and RXL (Relaton metadata).
27
+ . **Package & release** — `metanorma-release release` discovers compiled docs, extracts metadata from RXL, packages as ZIP, and publishes to GitHub Releases with channel labels derived from `metanorma.release.yml` routing rules.
28
+ . **Aggregate** — `metanorma-release aggregate` discovers repos by topic/topic, fetches their releases, filters by channel, extracts ZIP assets, enriches with Relaton bibliographic data, and writes `index.json` + file tree.
29
+ . **Present** — A site generator (Jekyll) reads `index.json` and renders the document registry.
23
30
 
24
31
  == Installation
25
32
 
@@ -45,20 +52,56 @@ Requires Ruby >= 3.2. Optional runtime dependencies:
45
52
 
46
53
  == Quick start
47
54
 
48
- === CLI
55
+ === In a document repository
56
+
57
+ Create `metanorma.release.yml`:
58
+
59
+ [source,yaml]
60
+ ----
61
+ documents:
62
+ - pattern: "cc-*"
63
+ channels: [public/standards]
64
+ ----
49
65
 
50
- The gem ships three commands:
66
+ Run the release:
51
67
 
52
68
  [source,sh]
53
69
  ----
54
- # Package compiled documents as zip archives
55
- metanorma-release package --output-dir _site
70
+ metanorma-release release --platform github --token $GITHUB_TOKEN
71
+ ----
72
+
73
+ === In an aggregator site
56
74
 
57
- # Package and release to a platform
58
- metanorma-release release --platform github --output-dir _site --token $GITHUB_TOKEN
75
+ Create `metanorma.aggregate.yml`:
59
76
 
60
- # Aggregate published releases into a file tree + index.json
61
- metanorma-release aggregate --repos my-org/my-repo --output-dir _site/cc
77
+ [source,yaml]
78
+ ----
79
+ source: github
80
+ output_dir: _site/docs
81
+ file_routing: flat
82
+
83
+ channels:
84
+ - public
85
+
86
+ display_categories:
87
+ - name: Standards
88
+ slug: standards
89
+ doctypes: [standard, specification, report]
90
+ - name: Guides
91
+ slug: guides
92
+ doctypes: [guide, advisory]
93
+
94
+ github:
95
+ organizations:
96
+ - MyOrg
97
+ topic: metanorma-release
98
+ ----
99
+
100
+ Run the aggregation:
101
+
102
+ [source,sh]
103
+ ----
104
+ metanorma-release aggregate
62
105
  ----
63
106
 
64
107
  === Ruby API
@@ -66,9 +109,8 @@ metanorma-release aggregate --repos my-org/my-repo --output-dir _site/cc
66
109
  [source,ruby]
67
110
  ----
68
111
  # Discover publications from compiled RXL files
69
- publications = Metanorma::Release::Publication.discover("_site")
112
+ publications = Metanorma::Release::RxlExtractor.discover("_site")
70
113
 
71
- # Each publication carries metadata from Relaton
72
114
  pub = publications.first
73
115
  pub.identifier # => "CC 18011:2018"
74
116
  pub.slug # => "cc-18011-2018"
@@ -78,13 +120,11 @@ pub.stage # => "60"
78
120
  pub.doctype # => "standard"
79
121
  pub.formats # => ["html", "pdf", "xml"]
80
122
 
81
- # Serialization (used in release body and sidecar metadata)
123
+ # Serialize (used in release body)
82
124
  pub.to_release_body # => "<!-- mn-release-metadata\n{...}\n-->"
83
- pub.to_json # => "{...}"
84
125
 
85
126
  # Parse from release body (used in aggregation)
86
127
  pub = Publication.from_release_body(body)
87
- pub = Publication.from_json(json_string)
88
128
  ----
89
129
 
90
130
  == CLI reference
@@ -124,7 +164,7 @@ metanorma-release release [options]
124
164
  |`--manifest FILE` |Release manifest file (default: `metanorma.release.yml`)
125
165
  |`--force` |Force release even if unchanged
126
166
  |`--force-replace PAT` |Glob pattern for forced replacement (repeatable)
127
- |`--channels CHANS` |Override channels
167
+ |`--channels CHANS` |Override channels (bypasses routing rules)
128
168
  |`--concurrency N` |Parallel workers (default: 4)
129
169
  |`--token TOKEN` |Platform auth token
130
170
  |`--config SOURCE` |Config file
@@ -148,110 +188,206 @@ metanorma-release aggregate [options]
148
188
  |`--source SOURCE` |Discovery source: `github`, `local:PATH` (default: `github`)
149
189
  |`--organizations ORGS` |Organization list (overrides config)
150
190
  |`--topic TOPIC` |Repository topic filter (default: `metanorma-release`)
151
- |`--repos REPOS` |Explicit repo list
152
- |`--channels CHANS` |Filter channels
153
- |`--stages STAGES` |Filter stages
191
+ |`--repos REPOS` |Explicit repo list (overrides discovery)
192
+ |`--channels CHANS` |Filter by channel (only aggregate matching channels)
154
193
  |`--output-dir DIR` |Output directory (default: `_site/cc`)
155
194
  |`--file-routing MODE` |File layout: `by-document`, `flat`, `by-format` (default: `by-document`)
156
- |`--[no-]include-drafts` |Include draft releases
195
+ |`--[no-]include-drafts` |Include draft releases (default: false)
157
196
  |`--concurrency N` |Parallel repos (default: 4)
158
197
  |`--min-documents N` |Fail if fewer documents found (default: 0)
159
198
  |`--token TOKEN` |Platform auth token (falls back to `GITHUB_TOKEN` env)
160
199
  |===
161
200
 
162
- ==== Aggregate config file
201
+ == Config files
202
+
203
+ === Per-repo: `metanorma.release.yml`
204
+
205
+ Defines how documents in a repository are routed to channels.
206
+ Placed in the root of each Metanorma document repository.
163
207
 
164
- Create `metanorma.aggregate.yml` in your project root:
208
+ ==== Simple single-channel
209
+
210
+ All documents go to one channel:
165
211
 
166
212
  [source,yaml]
167
213
  ----
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
214
+ documents:
215
+ - pattern: "cc-*"
216
+ channels: [public/standards]
177
217
  ----
178
218
 
179
- CLI flags override config file values. Cache is always enabled (defaults to `.cache/aggregate/`).
219
+ ==== Multi-channel by document pattern
180
220
 
181
- == Concepts
221
+ Route different documents to different channels based on their slug:
222
+
223
+ [source,yaml]
224
+ ----
225
+ documents:
226
+ - pattern: "cc-s-*"
227
+ channels: [public/standards]
228
+ - pattern: "cc-r-*"
229
+ channels: [public/reports]
230
+ - pattern: "cc-a-*"
231
+ channels: [public/admin]
232
+ - pattern: "cc-adv-*"
233
+ channels: [public/advisories]
234
+ ----
182
235
 
183
- === Publication
236
+ Pattern matching uses Ruby `File.fnmatch` glob syntax against the document slug.
237
+ The slug is derived from the document identifier: `CC 51020:2019` → `cc-51020-2019`.
184
238
 
185
- The central domain model. A `Publication` carries metadata from Relaton RXL extraction, files from the filesystem, and channels from config routing.
239
+ ==== Routing by stage and doctype
186
240
 
187
- [source,ruby]
188
- ----
189
- pub = Metanorma::Release::Publication.new(
190
- identifier: "CC 18011:2018",
191
- slug: "cc-18011-2018",
192
- title: "Date and time — Explicit representation",
193
- edition: "1",
194
- stage: "60",
195
- doctype: "standard",
196
- revdate: "2018-06-01",
197
- files: [PublicationFile.new(format: "html", name: "cc-18011.html", path: "cc-18011.html")],
198
- channels: ["public"]
199
- )
241
+ You can also route by stage or doctype instead of pattern:
200
242
 
201
- pub.base_dir # => "."
202
- pub.content_hash # => #<ContentHash ...>
203
- pub.with_channels(["members"]) # => new Publication with different channels
243
+ [source,yaml]
244
+ ----
245
+ documents:
246
+ - stage: ["20", "30"]
247
+ channels: [internal]
248
+ - doctypes: [standard, specification]
249
+ channels: [public/standards]
250
+ - doctypes: [report]
251
+ channels: [public/reports]
204
252
  ----
205
253
 
206
- === Channels
207
-
208
- Channels are simple string labels that control document visibility and distribution. Typical values: `public`, `members`, `internal`.
254
+ Multiple criteria are ANDed (a document must match all specified fields).
255
+ First matching entry wins. Documents not matching any entry default to `["public"]`.
209
256
 
210
- === Config
257
+ ==== Slug strategies
211
258
 
212
- A `metanorma.release.yml` config file defines channels and routing rules for an organization:
259
+ Tag naming varies by publisher. Set the default strategy and per-publisher overrides:
213
260
 
214
261
  [source,yaml]
215
262
  ----
216
- channels:
217
- - public
218
- - members
219
- - internal
220
-
221
- routing:
222
- default: [public]
223
- rules:
224
- - stage: ["20", "30"]
225
- channels: [internal]
226
- - stage: ["60"]
227
- channels: [public]
228
- - doctype: [report]
229
- channels: [public]
230
-
231
263
  slug:
232
264
  default: edition
233
265
  strategies:
234
266
  ietf: internet-draft
235
267
  ieee: draft-suffix
236
- iho: version
237
- ogc: version
238
268
  ----
239
269
 
240
- Routing rules match raw metadata values from Relaton (stage, doctype) to channel labels. When no config is present, all documents route to `public`.
270
+ [cols="1m,1,2",options="header"]
271
+ |===
272
+ |Strategy |Tag format |Used for
273
+ |`edition` (default) |`cc-18011-2018/ed1` |CalConnect, ISO
274
+ |`version` |`iho-s44/v1` |IHO, OGC
275
+ |`internet-draft` |`id-ietf-foo/1` |IETF drafts
276
+ |`rfc` |`rfc-1234/ed1` |IETF RFCs
277
+ |`draft-suffix` |`ieee-8021/d1` |IEEE
278
+ |===
241
279
 
242
- === Slug strategies
280
+ The strategy is resolved from the document identifier prefix (e.g., `CC` → default, `IETF` → `ietf`).
243
281
 
244
- Tag and file naming varies by publisher (derived from the document identifier prefix). Strategies are resolved via a registry:
282
+ === Per-site: `metanorma.aggregate.yml`
245
283
 
246
- [cols="1m,1,2",options="header"]
284
+ Defines how an aggregator site discovers, filters, and outputs documents.
285
+
286
+ [source,yaml]
287
+ ----
288
+ source: github
289
+ output_dir: _site/docs
290
+ file_routing: flat
291
+ cache_dir: .cache/aggregate
292
+ data_dir: _data
293
+
294
+ channels:
295
+ - public
296
+
297
+ include_drafts: true
298
+
299
+ display_categories:
300
+ - name: Standards, Specifications & Reports
301
+ slug: standards
302
+ doctypes:
303
+ - standard
304
+ - specification
305
+ - report
306
+ - name: Guides & Advisories
307
+ slug: guides
308
+ doctypes:
309
+ - guide
310
+ - advisory
311
+ - name: Directives
312
+ slug: directives
313
+ doctypes:
314
+ - directive
315
+ - name: Administrative
316
+ slug: administrative
317
+ doctypes:
318
+ - administrative
319
+
320
+ github:
321
+ organizations:
322
+ - CalConnect
323
+ topic: metanorma-release
324
+ ----
325
+
326
+ ==== Config reference
327
+
328
+ [cols="1m,1,3",options="header"]
247
329
  |===
248
- |Publisher |Strategy |Tag format
249
- |default (CalConnect, ISO) |`EditionSlug` |`cc-18011-2018/ed1`
250
- |IETF draft |`InternetDraftSlug` |`id-ietf-foo/1`
251
- |IETF RFC |`RfcSlug` |`rfc-1234/ed1`
252
- |IEEE |`DraftSuffixSlug` |`ieee-8021/d1`
253
- |IHO, OGC |`VersionSlug` |`iho-s44/v1`
330
+ |Key |Default |Description
331
+ |`source` |`github` |Discovery source: `github` or `local:PATH`
332
+ |`output_dir` |`_site/cc` |Where to write extracted files and `index.json`
333
+ |`file_routing` |`by-document` |File layout: `by-document`, `flat`, or `by-format`
334
+ |`cache_dir` |`.cache/aggregate` |Delta state cache for incremental builds
335
+ |`data_dir` |_none_ |If set, writes flattened `documents.json` for site generators
336
+ |`channels` |`[]` |Channel filter (empty = accept all channels)
337
+ |`include_drafts` |`false` |Whether to include draft-stage releases
338
+ |`display_categories` |`[]` |Maps doctypes to display categories for site output
339
+ |`github.organizations` |_required_ |GitHub orgs to scan for repositories
340
+ |`github.topic` |`metanorma-release` |Repository topic filter
341
+ |===
342
+
343
+ CLI flags override config file values.
344
+
345
+ === Global options
346
+
347
+ All commands accept these options:
348
+
349
+ [cols="1m,3",options="header"]
254
350
  |===
351
+ |Option |Description
352
+ |`--verbose` |Enable debug-level logging
353
+ |`--quiet` |Suppress warnings (only errors shown)
354
+ |`--help` |Show command help
355
+ |===
356
+
357
+ == Concepts
358
+
359
+ === Channels
360
+
361
+ Channels are hierarchical string labels that control document visibility.
362
+ They are assigned during release (per-repo config) and filtered during aggregation (per-site config).
363
+
364
+ Typical channel hierarchy:
365
+
366
+ ```
367
+ public/ → visible on public sites
368
+ public/standards → published standards
369
+ public/reports → technical reports
370
+ public/admin → administrative documents
371
+ members/ → members-only content
372
+ internal/ → not published to any site
373
+ ```
374
+
375
+ Channel matching is prefix-based: a filter for `public` matches `public/standards`, `public/reports`, etc.
376
+
377
+ === Display categories
378
+
379
+ Display categories map document types to site sections.
380
+ Defined in `metanorma.aggregate.yml`, they group doctypes into user-facing categories:
381
+
382
+ [source,yaml]
383
+ ----
384
+ display_categories:
385
+ - name: Standards, Specifications & Reports
386
+ slug: standards
387
+ doctypes: [standard, specification, report]
388
+ ----
389
+
390
+ The aggregator resolves each document's display category from its doctype and includes `display_category` and `display_category_slug` fields in the output JSON.
255
391
 
256
392
  === File routing
257
393
 
@@ -265,6 +401,97 @@ The aggregation pipeline supports three file layout modes:
265
401
  |`by-format` |`html/cc-18011.html`
266
402
  |===
267
403
 
404
+ === Delta state caching
405
+
406
+ Aggregation is incremental: a delta state cache tracks which repos/tags have already been processed.
407
+ On subsequent runs, unchanged repos are skipped entirely.
408
+ The cache lives in `.cache/aggregate/` by default and should be persisted in CI (cache action, artifact upload).
409
+
410
+ === Output format
411
+
412
+ The aggregator writes:
413
+
414
+ * `index.json` — full document index with metadata, bibliographic data, and file references
415
+ * File tree — extracted document files (HTML, PDF, XML, RXL) organized by file routing mode
416
+ * `_data/documents.json` — flattened version for Jekyll site generators (if `data_dir` is set)
417
+
418
+ Each document in the output includes:
419
+
420
+ ```
421
+ slug, id, title, abstract, stage, doctype, edition, date,
422
+ channels, formats, files,
423
+ has_html, has_pdf, has_xml, has_rxl,
424
+ html_path, pdf_path, xml_path, rxl_path,
425
+ stage_css, doctype_class,
426
+ display_category, display_category_slug,
427
+ authors, committee,
428
+ bibliographic
429
+ ```
430
+
431
+ == Creating a new document repository
432
+
433
+ === Using the template
434
+
435
+ For CalConnect documents, use the https://github.com/CalConnect/cc-template[`cc-template`] repository template:
436
+
437
+ . Click **Use this template** on GitHub
438
+ . Name the repo `cc-{descriptive-name}` (e.g. `cc-icalendar-series`)
439
+ . Replace placeholder document numbers in `sources/` and `metanorma.yml`
440
+ . Add the `metanorma-release` topic to the repository
441
+ . Push to `main`
442
+
443
+ === Manual setup
444
+
445
+ . Create a repository with the `metanorma-release` GitHub topic
446
+ . Add `metanorma.release.yml` with routing rules:
447
+
448
+ +
449
+ [source,yaml]
450
+ ----
451
+ documents:
452
+ - pattern: "cc-*"
453
+ channels: [public/standards]
454
+ ----
455
+
456
+ . Add `metanorma.yml` with source file list:
457
+
458
+ +
459
+ [source,yaml]
460
+ ----
461
+ metanorma:
462
+ source:
463
+ files:
464
+ - sources/cc-51020.adoc
465
+ collection:
466
+ name: "My Document Title"
467
+ organization: CalConnect
468
+ ----
469
+
470
+ . Add a CI workflow (`.github/workflows/release.yml`):
471
+
472
+ +
473
+ [source,yaml]
474
+ ----
475
+ name: Release
476
+ on:
477
+ push:
478
+ branches: [main]
479
+ paths: ['sources/**', 'metanorma.yml', 'metanorma.release.yml']
480
+ workflow_dispatch:
481
+ permissions:
482
+ contents: write
483
+ jobs:
484
+ release:
485
+ uses: actions-mn/.github/.github/workflows/metanorma-release.yml@main
486
+ with:
487
+ default-visibility: private
488
+ secrets: inherit
489
+ ----
490
+
491
+ . Write your AsciiDoc source under `sources/`
492
+
493
+ The aggregator site will automatically discover your repository and publish its documents.
494
+
268
495
  == Architecture
269
496
 
270
497
  === Domain model
@@ -274,9 +501,9 @@ All core types are immutable, frozen value objects:
274
501
  * `Publication` -- metadata + files + channels + source
275
502
  * `PublicationFile` -- format, name, path
276
503
  * `PublicationSource` -- owner, repo, tag, url, date
277
- * `Channel` -- string label wrapper
278
- * `Index` -- collection of Publications with parameters
279
- * `Site` -- aggregated output (index + file tree + Relaton enrichment)
504
+ * `Channel` -- string label wrapper with prefix matching
505
+ * `Index` -- collection of Publications with schema version
506
+ * `Site` -- aggregated output (index + file tree + Relaton enrichment + display categories)
280
507
 
281
508
  === Dependency flow
282
509