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.json with 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 for package and release)

  • octokit — GitHub platform adapter (required for GitHub releases/aggregation)

  • rubyzip — zip packaging (required for package and release)

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

--output-dir DIR

Directory containing compiled documents (default: _site)

--dest DIR

Destination for zip packages (default: dist)

--manifest FILE

Release manifest file (default: metanorma.release.yml)

--config SOURCE

Config file

metanorma-release release

Package and release documents to a platform.

metanorma-release release [options]
Option Description

--platform NAME

Target platform: github, local, null (default: github)

--output-dir DIR

Compiled docs directory (default: _site)

--manifest FILE

Release manifest file (default: metanorma.release.yml)

--force

Force release even if unchanged

--force-replace PAT

Glob pattern for forced replacement (repeatable)

--channels CHANS

Override channels

--concurrency N

Parallel workers (default: 4)

--token TOKEN

Platform auth token

--config SOURCE

Config file

metanorma-release aggregate

Aggregate published releases from multiple repositories into a unified file tree.

metanorma-release aggregate [options]
Option Description

--source SOURCE

Discovery source: github, local:PATH (default: github)

--organizations ORGS

Organization list

--topic TOPIC

Repository topic filter (default: metanorma-release)

--repos REPOS

Explicit repo list

--channels CHANS

Filter channels

--stages STAGES

Filter stages

--output-dir DIR

Output directory (default: _site/cc)

--file-routing MODE

File layout: by-document, flat, by-format (default: by-document)

--cache-dir DIR

Cache directory for delta state

--[no-]include-drafts

Include draft releases

--concurrency N

Parallel repos (default: 4)

--min-documents N

Fail if fewer documents found (default: 0)

--token TOKEN

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

default (CalConnect, ISO)

EditionSlug

cc-18011-2018/ed1

IETF draft

InternetDraftSlug

id-ietf-foo/1

IETF RFC

RfcSlug

rfc-1234/ed1

IEEE

DraftSuffixSlug

ieee-8021/d1

IHO, OGC

VersionSlug

iho-s44/v1

File routing

The aggregation pipeline supports three file layout modes:

Mode Example path

by-document (default)

cc-18011/cc-18011.html

flat

cc-18011.html

by-format

html/cc-18011.html

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 platform/ with Publisher, Discoverer, Fetcher, ManifestReader classes that include the corresponding interface modules; register in PlatformFactory

A new slug strategy

Create a class that includes SlugStrategy; register via SlugRegistry#register

A new file routing mode

Create a class that includes FileRouting with a #compute_path method; register in FileRoutingFactory

A new filter

Create a class that includes Filter; pass to the pipeline’s filters array

Development

bundle install
bundle exec rspec
bundle exec rubocop

License

BSD-2-Clause. See LICENSE for details.