Release lifecycle management for Metanorma documents.
Overview
metanorma-release manages the full release lifecycle of Metanorma documents through two config files:
- Per-repo (
metanorma.release.yml) -
Defines routing rules that map documents to channel labels based on slug pattern, stage, or doctype. Read by the
releasecommand to tag releases with channel metadata. - Per-site (
metanorma.aggregate.yml) -
Defines discovery (which repos to aggregate), channel subscription (which channels to include), display categories, and output layout. Read by the
aggregatecommand to build a file tree +index.jsonfor any site generator.
The output is platform-agnostic: a directory containing index.json and a tree of document files (HTML, PDF, XML, RXL).
Any site generator (Jekyll, Hugo, Vite) consumes that output independently.
How it works
-
Compile — Metanorma compiles
.adocsources to HTML, PDF, XML, and RXL (Relaton metadata). -
Package & release —
metanorma-release releasediscovers compiled docs, extracts metadata from RXL, packages as ZIP, and publishes to GitHub Releases with channel labels derived frommetanorma.release.ymlrouting rules. -
Aggregate —
metanorma-release aggregatediscovers repos by topic/topic, fetches their releases, filters by channel, extracts ZIP assets, enriches with Relaton bibliographic data, and writesindex.json+ file tree. -
Present — A site generator (Jekyll) reads
index.jsonand renders the document registry.
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
In a document repository
Create metanorma.release.yml:
documents:
- pattern: "cc-*"
channels: [public/standards]
Run the release:
metanorma-release release --platform github --token $GITHUB_TOKEN
In an aggregator site
Create metanorma.aggregate.yml:
source: github
output_dir: _site/docs
file_routing: flat
channels:
- public
display_categories:
- name: Standards
slug: standards
doctypes: [standard, specification, report]
- name: Guides
slug: guides
doctypes: [guide, advisory]
github:
organizations:
- MyOrg
topic: metanorma-release
Run the aggregation:
metanorma-release aggregate
Ruby API
# Discover publications from compiled RXL files
publications = Metanorma::Release::Publication.discover("_site")
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"]
# Serialize (used in release body)
pub.to_release_body # => "<!-- mn-release-metadata\n{...}\n-->"
# Parse from release body (used in aggregation)
pub = Publication.from_release_body(body)
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 (bypasses routing rules) |
|
Parallel workers (default: 4) |
|
Platform auth token |
|
Config file |
metanorma-release aggregate
Aggregate published releases from multiple repositories into a unified file tree.
Reads config from metanorma.aggregate.yml if present (auto-detected).
Idempotent: uses cached delta state (.cache/aggregate/) to skip unchanged repos.
metanorma-release aggregate [options]
| Option | Description |
|---|---|
|
Config file (default: |
|
Discovery source: |
|
Organization list (overrides config) |
|
Repository topic filter (default: |
|
Explicit repo list (overrides discovery) |
|
Filter by channel (only aggregate matching channels) |
|
Output directory (default: |
|
File layout: |
|
Include draft releases (default: false) |
|
Parallel repos (default: 4) |
|
Fail if fewer documents found (default: 0) |
|
Platform auth token (falls back to |
Config files
Per-repo: metanorma.release.yml
Defines how documents in a repository are routed to channels. Placed in the root of each Metanorma document repository.
Simple single-channel
All documents go to one channel:
documents:
- pattern: "cc-*"
channels: [public/standards]
Multi-channel by document pattern
Route different documents to different channels based on their slug:
documents:
- pattern: "cc-s-*"
channels: [public/standards]
- pattern: "cc-r-*"
channels: [public/reports]
- pattern: "cc-a-*"
channels: [public/admin]
- pattern: "cc-adv-*"
channels: [public/advisories]
Pattern matching uses Ruby File.fnmatch glob syntax against the document slug.
The slug is derived from the document identifier: CC 51020:2019 → cc-51020-2019.
Routing by stage and doctype
You can also route by stage or doctype instead of pattern:
documents:
- stages: ["20", "30"]
channels: [internal]
- doctypes: [standard, specification]
channels: [public/standards]
- doctypes: [report]
channels: [public/reports]
Multiple criteria are ANDed (a document must match all specified fields).
First matching entry wins. Documents not matching any entry default to ["public"].
Slug strategies
Tag naming varies by publisher. Set the default strategy and per-publisher overrides:
slug:
default: edition
strategies:
ietf: internet-draft
ieee: draft-suffix
| Strategy | Tag format | Used for |
|---|---|---|
|
|
CalConnect, ISO |
|
|
IHO, OGC |
|
|
IETF drafts |
|
|
IETF RFCs |
|
|
IEEE |
The strategy is resolved from the document identifier prefix (e.g., CC → default, IETF → ietf).
Per-site: metanorma.aggregate.yml
Defines how an aggregator site discovers, filters, and outputs documents.
source: github
output_dir: _site/docs
file_routing: flat
cache_dir: .cache/aggregate
data_dir: _data
channels:
- public
include_drafts: true
display_categories:
- name: Standards, Specifications & Reports
slug: standards
doctypes:
- standard
- specification
- report
- name: Guides & Advisories
slug: guides
doctypes:
- guide
- advisory
- name: Directives
slug: directives
doctypes:
- directive
- name: Administrative
slug: administrative
doctypes:
- administrative
github:
organizations:
- CalConnect
topic: metanorma-release
Config reference
| Key | Default | Description |
|---|---|---|
|
|
Discovery source: |
|
|
Where to write extracted files and |
|
|
File layout: |
|
|
Delta state cache for incremental builds |
|
none |
If set, writes flattened |
|
|
Channel filter (empty = accept all channels) |
|
|
Whether to include draft-stage releases |
|
|
Maps doctypes to display categories for site output |
|
required |
GitHub orgs to scan for repositories |
|
|
Repository topic filter |
CLI flags override config file values.
Concepts
Channels
Channels are hierarchical string labels that control document visibility. They are assigned during release (per-repo config) and filtered during aggregation (per-site config).
Typical channel hierarchy:
public/ → visible on public sites
public/standards → published standards
public/reports → technical reports
public/admin → administrative documents
members/ → members-only content
internal/ → not published to any site
Channel matching is prefix-based: a filter for public matches public/standards, public/reports, etc.
Display categories
Display categories map document types to site sections.
Defined in metanorma.aggregate.yml, they group doctypes into user-facing categories:
display_categories:
- name: Standards, Specifications & Reports
slug: standards
doctypes: [standard, specification, report]
The aggregator resolves each document’s display category from its doctype and includes display_category and display_category_slug fields in the output JSON.
File routing
The aggregation pipeline supports three file layout modes:
| Mode | Example path |
|---|---|
|
|
|
|
|
|
Delta state caching
Aggregation is incremental: a delta state cache tracks which repos/tags have already been processed.
On subsequent runs, unchanged repos are skipped entirely.
The cache lives in .cache/aggregate/ by default and should be persisted in CI (cache action, artifact upload).
Output format
The aggregator writes:
-
index.json— full document index with metadata, bibliographic data, and file references -
File tree — extracted document files (HTML, PDF, XML, RXL) organized by file routing mode
-
_data/documents.json— flattened version for Jekyll site generators (ifdata_diris set)
Each document in the output includes:
slug, id, title, abstract, stage, doctype, edition, date,
channels, formats, files,
has_html, has_pdf, has_xml, has_rxl,
html_path, pdf_path, xml_path, rxl_path,
stage_css, doctype_class,
display_category, display_category_slug,
authors, committee,
bibliographic
Creating a new document repository
Using the template
For CalConnect documents, use the cc-template repository template:
-
Click Use this template on GitHub
-
Name the repo
cc-{descriptive-name}(e.g.cc-icalendar-series) -
Replace placeholder document numbers in
sources/andmetanorma.yml -
Add the
metanorma-releasetopic to the repository -
Push to
main
Manual setup
-
Create a repository with the
metanorma-releaseGitHub topic -
Add
metanorma.release.ymlwith routing rules:documents: - pattern: "cc-*" channels: [public/standards] -
Add
metanorma.ymlwith source file list:metanorma: source: files: - sources/cc-51020.adoc collection: name: "My Document Title" organization: CalConnect -
Add a CI workflow (
.github/workflows/release.yml):name: Release on: push: branches: [main] paths: ['sources/**', 'metanorma.yml', 'metanorma.release.yml'] workflow_dispatch: permissions: contents: write jobs: release: uses: actions-mn/.github/.github/workflows/metanorma-release.yml@main with: default-visibility: private secrets: inherit -
Write your AsciiDoc source under
sources/
The aggregator site will automatically discover your repository and publish its documents.
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 with prefix matching -
Index— collection of Publications with schema version -
Site— aggregated output (index + file tree + Relaton enrichment + display categories)
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.