Release lifecycle management for Metanorma documents.
Overview
metanorma-release manages the full release lifecycle of Metanorma documents through three actors:
- Doc repo (producer)
-
Discover compiled documents → extract metadata from RXL → detect changes → package as zip → release to a platform (GitHub Releases, local filesystem).
- Org/publisher (governor)
-
Define channels and routing rules via config. Map document metadata (stage, doctype) to channel labels that control visibility and distribution.
- Aggregator (consumer)
-
Discover repositories → fetch published releases → filter by channel and stage → extract zip assets → generate
index.jsonwith Relaton enrichment and a file tree for any site generator.
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.
Installation
Add to your Gemfile:
gem "metanorma-release"
Or install directly:
gem install metanorma-release
Requires Ruby >= 3.2. Optional runtime dependencies:
-
relaton-bib— RXL metadata extraction (required forpackageandrelease) -
octokit— GitHub platform adapter (required for GitHub releases/aggregation) -
rubyzip— zip packaging (required forpackageandrelease)
Quick start
CLI
The gem ships three commands:
# Package compiled documents as zip archives
metanorma-release package --output-dir _site
# Package and release to a platform
metanorma-release release --platform github --output-dir _site --token $GITHUB_TOKEN
# Aggregate published releases into a file tree + index.json
metanorma-release aggregate --repos my-org/my-repo --output-dir _site/cc
Ruby API
# Discover publications from compiled RXL files
publications = Metanorma::Release::Publication.discover("_site")
# Each publication carries metadata from Relaton
pub = publications.first
pub.identifier # => "CC 18011:2018"
pub.slug # => "cc-18011-2018"
pub.title # => "Date and time — Explicit representation"
pub.edition # => "1"
pub.stage # => "60"
pub.doctype # => "standard"
pub.formats # => ["html", "pdf", "xml"]
# Serialization (used in release body and sidecar metadata)
pub.to_release_body # => "<!-- mn-release-metadata\n{...}\n-->"
pub.to_json # => "{...}"
# Parse from release body (used in aggregation)
pub = Publication.from_release_body(body)
pub = Publication.from_json(json_string)
CLI reference
metanorma-release package
Package compiled documents into zip archives without publishing.
metanorma-release package [options]
| Option | Description |
|---|---|
|
Directory containing compiled documents (default: |
|
Destination for zip packages (default: |
|
Release manifest file (default: |
|
Config file |
metanorma-release release
Package and release documents to a platform.
metanorma-release release [options]
| Option | Description |
|---|---|
|
Target platform: |
|
Compiled docs directory (default: |
|
Release manifest file (default: |
|
Force release even if unchanged |
|
Glob pattern for forced replacement (repeatable) |
|
Override channels |
|
Parallel workers (default: 4) |
|
Platform auth token |
|
Config file |
metanorma-release aggregate
Aggregate published releases from multiple repositories into a unified file tree.
metanorma-release aggregate [options]
| Option | Description |
|---|---|
|
Discovery source: |
|
Organization list |
|
Repository topic filter (default: |
|
Explicit repo list |
|
Filter channels |
|
Filter stages |
|
Output directory (default: |
|
File layout: |
|
Cache directory for delta state |
|
Include draft releases |
|
Parallel repos (default: 4) |
|
Fail if fewer documents found (default: 0) |
|
Platform auth token |
Concepts
Publication
The central domain model. A Publication carries metadata from Relaton RXL extraction, files from the filesystem, and channels from config routing.
pub = Metanorma::Release::Publication.new(
identifier: "CC 18011:2018",
slug: "cc-18011-2018",
title: "Date and time — Explicit representation",
edition: "1",
stage: "60",
doctype: "standard",
revdate: "2018-06-01",
files: [PublicationFile.new(format: "html", name: "cc-18011.html", path: "cc-18011.html")],
channels: ["public"]
)
pub.base_dir # => "."
pub.content_hash # => #<ContentHash ...>
pub.with_channels(["members"]) # => new Publication with different channels
Channels
Channels are simple string labels that control document visibility and distribution. Typical values: public, members, internal.
Config
A metanorma.release.yml config file defines channels and routing rules for an organization:
channels:
- public
- members
- internal
routing:
default: [public]
rules:
- stage: ["20", "30"]
channels: [internal]
- stage: ["60"]
channels: [public]
- doctype: [report]
channels: [public]
slug:
default: edition
strategies:
ietf: internet-draft
ieee: draft-suffix
iho: version
ogc: version
Routing rules match raw metadata values from Relaton (stage, doctype) to channel labels. When no config is present, all documents route to public.
Slug strategies
Tag and file naming varies by publisher (derived from the document identifier prefix). Strategies are resolved via a registry:
| Publisher | Strategy | Tag format |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
File routing
The aggregation pipeline supports three file layout modes:
| Mode | Example path |
|---|---|
|
|
|
|
|
|
Architecture
Domain model
All core types are immutable, frozen value objects:
-
Publication— metadata + files + channels + source -
PublicationFile— format, name, path -
PublicationSource— owner, repo, tag, url, date -
Channel— string label wrapper -
Index— collection of Publications with parameters -
Site— aggregated output (index + file tree + Relaton enrichment)
Dependency flow
Unidirectional, no cycles:
domain/ -> pipelines/ -> platform/
-> cli/commands/
-
Domain models have zero knowledge of pipelines, platforms, or CLI
-
Pipelines receive all dependencies through constructors (dependency injection)
-
Platform adapters implement interface modules (Publisher, RepoDiscoverer, ReleaseFetcher, etc.)
-
CLI delegates to command classes; commands construct pipelines
Patterns
- Value Objects
-
Immutable, frozen, value-based equality via
eql?/hash. - Strategy Pattern
-
Pluggable slug strategies resolved via registry. Adding a new publisher type requires zero changes to existing code.
- Pipeline with DI
-
Pipelines receive all dependencies through constructors. No global state, no service locators.
- Interface Modules
-
Type contracts using
include Module— not duck typing. Dependencies are validated at construction time. - Null Object
-
Disabled features inject null implementations (
NullDeltaState,NullPublisher,NullCacheStore). - Result Types
-
Pipelines return frozen Structs. Errors are collected, not raised.
Extending
| To add… | Do this |
|---|---|
A new platform |
Create a directory under |
A new slug strategy |
Create a class that includes |
A new file routing mode |
Create a class that includes |
A new filter |
Create a class that includes |
Development
bundle install
bundle exec rspec
bundle exec rubocop
License
BSD-2-Clause. See LICENSE for details.