Metanorma: Aequitate Verum

Getting Started with Metanorma in CI/CD Environments

Introduction

Continuous Integration (CI) and Continuous Deployment (CD) are crucial for managing and authoring documents with Metanorma.

CI/CD automates the process of building, testing, and deploying documents, ensuring that they are always up-to-date and consistent. This automation reduces manual errors, speeds up the release cycle, and allows for continuous feedback and improvement. By integrating Metanorma into CI/CD pipelines, users can streamline their document workflows, maintain high-quality standards, and focus on content creation rather than manual processes.

This guide provides an overview of how to integrate Metanorma into CI/CD platforms and run Metanorma in a CI/CD environment.

Principles

Integration of Metanorma into CI/CD environments consists of two phases:

Metanorma provides a wide set of Docker images on Docker Hub.

Supported platforms

Metanorma provides different levels of CI/CD support depending on the platform:

Platform Compilation Site deployment Organization-scale publishing

GitHub Actions

Yes (native actions)

Yes (GitHub Pages)

Yes (per-document releases, channels, aggregation)

GitLab CI

Yes (Docker)

Yes (GitLab Pages)

No

Travis CI

Yes (Docker)

Manual

No

CircleCI

Yes (Docker)

Manual

No

Jenkins

Yes (Docker)

Manual

No

Bitrise

Yes (Docker)

Manual

No

GitHub provides the most complete support through the actions-mn ecosystem of GitHub Actions.

Using Metanorma on GitHub

Metanorma provides a set of composable GitHub Actions at actions-mn. Each action handles one stage of the document lifecycle.

The actions-mn ecosystem

Action Role Used in

actions-mn/setup::

Installs the metanorma CLI for Ubuntu, MacOS, or Windows. Skip if using the metanorma/metanorma Docker image (Metanorma is already installed).

Any workflow

actions-mn/cache::

Caches files used by and generated by Metanorma during compilation (temporary fonts, Relaton data, site output) to speed up subsequent runs.

Any build workflow

actions-mn/compile::

Compiles a single Metanorma document to HTML/PDF/XML/etc.

Single-document repos

actions-mn/site-gen::

Compiles all documents from a metanorma.yml manifest using metanorma site generate.

Any repo with multiple documents

actions-mn/build-and-publish::

Meta-action: calls cache then site-gen then uploads a Pages artifact. Combines three steps into one.

Simple site deployments

actions-mn/deploy-pages::

Deploys to GitHub Pages with PR preview support. Handles three scenarios: push to main (deploy), PR open/update (preview), PR close (cleanup).

Any Pages deployment

actions-mn/release::

Discovers compiled documents, detects changes, and publishes them as per-document GitHub Releases with channel-based routing and stage gating.

Organization-scale publishing

actions-mn/aggregate::

Aggregates released Metanorma documents from multiple GitHub repositories with channel-based filtering. Discovers repos by topic, downloads artifacts, and generates a structured index.

Portal repositories

actions-mn/setup-flavors::

Installs extra or private Metanorma flavor gems (e.g., BSI, NIST, Plateau).

Repos using non-standard flavors

Pipeline patterns

Two pipeline patterns cover all use cases:

Simple site (single repo)

Use build-and-publish + deploy-pages. On every push, compile the site and deploy it. This is what most single-team or single-document projects use.

Organization-scale (multi-repo)

Use site-gen + release in each per-document repository, and aggregate in the portal repository. Each repo publishes its own documents as GitHub Releases. The portal discovers and collects them.

The organization-scale pipeline relies on GitHub Releases and GitHub Topics — features that are only available on GitHub.

Simple site workflow

The following workflow compiles a Metanorma site and deploys it to GitHub Pages.

Example 1. .github/workflows/generate.yml file for generating a Metanorma site and deploying it to GitHub Pages
name: generate

on:
  push:
    branches: [ main ]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    container:
      image: metanorma/metanorma:latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Cache Metanorma assets
        uses: actions-mn/cache@v1

      - name: Metanorma generate site
        uses: actions-mn/build-and-publish@main
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          agree-to-terms: true

  deploy:
    if: ${{ github.ref == 'refs/heads/main' }}
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v1

For complete workflow examples, please refer to the documentation of the actions-mn/build-and-publish action.

Per-document repository structure

A repository that participates in the organization-scale pipeline follows this canonical structure:

my-documents/
  .github/workflows/
    release.yml            ← compiles and releases on push to main
    generate.yml           ← builds preview site on push and PRs
  .metanorma/
    channels.yml           ← declares which channels this repo publishes
  sources/
    cc-s-51015.adoc        ← AsciiDoc authoring sources
    cc-s-51024.adoc
  metanorma.yml            ← site/collection config (lists source files)
  metanorma.release.yml    ← release manifest (channel routing + stage gating)
  Gemfile                  ← typically just "metanorma-cli"

Each file’s role:

metanorma.yml

Tells metanorma site generate which source files to compile and what collection metadata to use.

metanorma.release.yml

Controls which documents get released, to which channels, and at which stages. Uses pattern-based matching so new documents are automatically included.

.metanorma/channels.yml

Declares which channels the repository publishes. Aggregators read this file to skip repos that don’t match the requested channels.

.github/workflows/release.yml

Compiles documents and publishes them as GitHub Releases.

.github/workflows/generate.yml

Builds a preview site on every push and PR for review before merging.

Reusable workflows

Per-document repositories can use reusable workflows from actions-mn/.github to avoid defining their own build logic:

actions-mn/.github/.github/workflows/metanorma-release.yml

Checks out the repo, runs actions-mn/site-gen@v1 to compile documents, then runs actions-mn/release@v1 to publish changed documents as GitHub Releases.

actions-mn/.github/.github/workflows/metanorma-generate.yml

Checks out the repo inside the metanorma/metanorma Docker container, runs actions-mn/cache@v1 and actions-mn/build-and-publish@v1, then deploys the preview to GitHub Pages.

A per-document repository’s entire release workflow is:

# .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

Publication pipeline (per-document releases)

The actions-mn/release and actions-mn/aggregate actions implement a channel-based publication architecture for organizations that publish many documents across many repositories.

When to use this

Use the release + aggregate pipeline when you have:

  • Multiple document repositories that each produce one or more documents

  • A portal site that aggregates documents from all repositories

  • Different document types (standards, reports, directives) that should appear on different portals or in different sections

  • A need to ensure that internal or draft documents never appear on public portals

For single-document sites, actions-mn/build-and-publish is simpler and sufficient.

Architecture

The publication pipeline has three stages:

Author          Per-document repo       GitHub            Portal repo
sources/        .github/workflows       Releases          .github/workflows
*.adoc    →     site-gen + release  →   per-document  →   aggregate      →  site
                                          releases          + index.json
Component Role Where

metanorma site generate

Compiles AsciiDoc sources to HTML/PDF/XML/RXL

Per-document repo (CI)

actions-mn/release

Discovers compiled documents, detects changes, publishes per-document GitHub Releases

Per-document repo (CI)

GitHub Releases

Stores release artifacts (zip) + metadata (channels, stage, edition)

GitHub

actions-mn/aggregate

Discovers repos by topic, filters by channel, downloads and indexes released documents

Portal repo (CI)

The metanorma-release GitHub topic

The metanorma-release topic is a GitHub repository topic that signals "this repo publishes Metanorma documents via GitHub Releases." It is the mechanism that makes portal aggregation zero-maintenance.

The actions-mn/aggregate action discovers participating repositories by searching for this topic:

search/repositories?q=topic:metanorma-release+org:{organization}

To add the topic to a repository:

gh api repos/{owner}/{repo}/topics -X PUT --field names='["metanorma-release"]'

If topic-based discovery is not suitable (e.g., aggregating repos from multiple organizations or external sources), use the repos input instead:

- uses: actions-mn/aggregate@v1
  with:
    repos: 'my-org/repo-a,other-org/repo-b'
    channels: 'public/standards'

Channels

A channel is an audience/category pair that determines where a document appears:

  • audience: public, members, or internal — who can see it

  • category: free-form identifier — where it appears in the portal

public/standards        ← published standards, visible to everyone
public/reports          ← conference and technical reports
members/internal-review ← only visible to organization members
internal/working-draft  ← never aggregated by any external portal

The publisher sets the channel. The aggregator filters by it. A portal cannot override or discover channels the publisher didn’t assign.

Per-document repo setup

Release manifest (metanorma.release.yml)

The release manifest maps documents to channels using patterns:

# metanorma.release.yml
documents:
  - pattern: "cc-s-*"
    channels: [public/standards]
  - pattern: "cc-r-*"
    channels: [public/reports]
  - pattern: "cc-a-*"
    channels: [public/admin]

When an author adds a new document whose ID matches a pattern (e.g. cc-s-51020), it is automatically assigned to the corresponding channel. No manifest update needed.

For single-document repos or exceptions, use source instead:

documents:
  - source: sources/cc-10001.adoc
    channels: [public/directives]

Stage gating prevents drafts from being published:

documents:
  - pattern: "cc-s-*"
    stages: [published]        # only published stage creates a release
    channels: [public/standards]
Channel discovery manifest (.metanorma/channels.yml)

This file declares which channels a repository publishes, enabling aggregators to skip repos that don’t match the requested channels:

# .metanorma/channels.yml
channels:
  - name: public/standards
    description: "Published CalConnect standards from this repo"
Release workflow
# .github/workflows/release.yml
name: Release

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  release:
    uses: actions-mn/.github/.github/workflows/metanorma-release.yml@main
    with:
      default-visibility: public    # documents not in manifest default to public
    secrets: inherit

This reusable workflow runs metanorma site generate to compile documents, then actions-mn/release to publish changed documents as GitHub Releases.

Document stages

Every Metanorma document has a stage, extracted from its RXL metadata:

Stage ISO code

Working Draft

20

Committee Draft

30

Draft

40

Published

60

Withdrawn

95

Stage filtering works at two levels:

Manifest-level stage gating — restricts which stages trigger a release for specific document patterns:

documents:
  - pattern: "cc-s-*"
    stages: [published]        # only published documents get released
    channels: [public/standards]

CI-level stage override — filters at the workflow level, narrowing the manifest policy for a specific run:

- uses: actions-mn/release@v1
  with:
    stages: 'published,final-draft'

Published releases are immutable — the tag is created once and never overwritten. Draft releases are rolling — the same tag is updated in-place as the draft evolves.

Portal repo setup

The portal uses actions-mn/aggregate to discover repos, download released documents, filter by channel, and generate a structured index.

# .github/workflows/build_deploy.yml
name: Build and Deploy

on:
  push:
    branches: [main]
  workflow_dispatch:
  schedule:
    - cron: '0 6 * * *'    # daily refresh

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/cache@v4
        with:
          path: .cache/aggregate
          key: mn-aggregate-${{ github.run_id }}
          restore-keys: mn-aggregate-

      - name: Aggregate released documents
        uses: actions-mn/aggregate@v1
        id: aggregate
        with:
          organizations: MyOrg
          topic: metanorma-release
          output-dir: _site/documents
          channels: 'public/standards,public/reports'
          canonicalize: true
          cache-dir: .cache/aggregate
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Build site
        run: bundle exec jekyll build

      - uses: actions/upload-pages-artifact@v3
Aggregate action inputs
Input Default Description

organizations

''

Comma-separated GitHub organizations to scan for repos

topic

metanorma-release

Repository topic for auto-discovery

repos

''

Explicit repo list (owner/repo), skips topic discovery

channels

''

Comma-separated channels to include. Empty = all.

stages

''

Comma-separated stages to include. Empty = all.

output-dir

_site/documents

Directory for extracted document files

canonicalize

true

Strip edition suffixes from filenames

concurrency

4

Max parallel repo processing

cache-dir

''

Directory for persistent cache (ETags, delta state)

token

${{ github.token }}

GitHub token for API access

Release metadata protocol

Each GitHub Release published by actions-mn/release carries structured metadata:

content-hash:abc123...

<!-- mn-release-metadata
{"version":1,"id":"cc-s-51015","channels":["public/standards"],
 "stage":"published","edition":"1","title":"My Standard"}
 -->

## CC/S 51015

| Field | Value |
|---|---|
| Document | cc-s-51015 |
| Edition | 1 |
| Status | published |
| Channels | public/standards |

The content-hash on the first line enables incremental aggregation — unchanged releases are skipped. The mn-release-metadata JSON block (inside an HTML comment) is parsed by actions-mn/aggregate for channel filtering and indexing.

Release action inputs

Input Default Description

source-path

.

Source path containing the metanorma configuration

output-dir

_site

Output directory containing compiled documents

release-config

metanorma.release.yml

Release manifest file

default-visibility

public

Default visibility for unlisted documents (public, private, or members)

force

false

Force release even if content hash matches

force-replace

''

Comma-separated doc IDs or glob patterns to force-replace (deletes and recreates specific releases)

include-pattern

*

Glob pattern to filter documents for release

stages

''

Comma-separated stages to release. Empty = all.

channels

''

Override channels for all documents. Empty = use manifest.

concurrency

4

Max parallel document processing

token

${{ github.token }}

GitHub token for creating releases

Force-replacing releases

Published releases are immutable by default — the action will not overwrite an existing release. To selectively re-release a specific document (e.g. to fix bad metadata), use the force-replace input:

- uses: actions-mn/release@v1
  with:
    force-replace: 'cc-s-51015'       # exact doc ID
    # or: force-replace: 'cc-s-*'     # glob pattern
    token: ${{ secrets.GITHUB_TOKEN }}

Only matched documents are deleted and recreated. Other documents in the same repo are completely unaffected.

File routing

actions-mn/aggregate supports three output structures via file-routing:

flat (default)

All files in output-dir/

by-doctype

{output-dir}/article/ subdirectories

by-format

{output-dir}/{ext}/ subdirectories

Generated index

The aggregate action writes index.json to the output directory:

{
  "version": 1,
  "generatedAt": "2026-05-13T06:00:00Z",
  "parameters": {
    "organizations": ["MyOrg"],
    "channels": ["public/standards"],
    "topic": "metanorma-release"
  },
  "summary": {
    "repoCount": 5,
    "documentCount": 42,
    "channelsFound": ["public/standards"]
  },
  "documents": [
    {
      "id": "cc-s-51015",
      "title": "My Standard",
      "channels": ["public/standards"],
      "files": [
        {"name": "cc-s-51015.html", "path": "cc-s-51015.html"},
        {"name": "cc-s-51015.pdf", "path": "cc-s-51015.pdf"}
      ]
    }
  ]
}

Using Metanorma on GitLab

GitLab CI is a continuous integration service built into GitLab that allows you to automate your workflow directly from your GitLab repository.

To use Metanorma in GitLab CI, you need to create a .gitlab-ci.yml file in your repository. The .gitlab-ci.yml file defines the steps that GitLab CI will run when a specific event occurs, such as a push to a branch or a merge request.

Metanorma uses the metanorma/metanorma Docker image to run Metanorma in GitLab CI.

Metanorma provides a set of reusable GitLab CI configurations at the metanorma/ci repository (on GitHub, not GitLab).

The workflows can be reused as follows.

Example 2. .gitlab-ci.yml file for generating a Metanorma site and deploying it to GitLab Pages through workflow inclusion
include:
- remote: 'https://raw.githubusercontent.com/metanorma/ci/main/cimas-config/gitlab-ci/samples/docker.shared.yml'

The following is an example of a full .gitlab-ci.yml file that generates a Metanorma site and deploys it to GitLab Pages, allowing for customization.

Example 3. .gitlab-ci.yml file for generating a Metanorma site and deploying it to GitLab Pages
image:
  name: metanorma/metanorma
  entrypoint: [ "" ]

cache:
  paths:

stages:
  - build
  - deploy

build:
  stage: build
  script:
    - curl -L --retry 3 https://raw.githubusercontent.com/metanorma/ci/main/gemfile-to-bundle-add.sh | bash
    - bundle install
    - metanorma site generate --output-dir public --agree-to-terms .

  artifacts:
    paths:
      - public

pages:
  dependencies:
    - build
  stage: deploy
  script:
    - |
      curl --location --output artifacts.zip --header "JOB-TOKEN: $CI_JOB_TOKEN" \
          "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/jobs/artifacts/master/download?job=build"
  artifacts:
    paths:
      - public
  only:
    - master
    - main

Using Metanorma on other platforms

Metanorma can be used on other CI/CD platforms by leveraging Docker. The following examples demonstrate how to set up Metanorma on Travis CI, CircleCI, Jenkins, and Bitrise using the metanorma/metanorma Docker image.

These platforms support compilation and artifact generation. Site deployment must be configured manually.

Travis CI

Example 4. .travis.yml file for generating a Metanorma site
language: minimal

services:
  - docker

before_install:
  - docker pull metanorma/metanorma

script:
  - docker run --rm -v $(pwd):/metanorma metanorma/metanorma metanorma site generate --agree-to-terms

CircleCI

Example 5. .circleci/config.yml file for generating a Metanorma site
version: 2.1

jobs:
  build:
    docker:
      - image: metanorma/metanorma
    steps:
      - checkout
      - run:
          name: Generate Metanorma site
          command: metanorma site generate --agree-to-terms

workflows:
  version: 2
  build:
    jobs:
      - build

Jenkins

Example 6. Jenkinsfile for generating a Metanorma site
pipeline {
    agent {
        docker {
            image 'metanorma/metanorma'
        }
    }
    stages {
        stage('Build') {
            steps {
                sh 'metanorma site generate --agree-to-terms'
            }
        }
    }
}

Bitrise

Example 7. bitrise.yml file for generating a Metanorma site
format_version: '8'
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git

workflows:
  primary:
    steps:
      - git-clone: {}
      - docker-compose:
          inputs:
            - content: |
                version: '3'
                services:
                  metanorma:
                    image: metanorma/metanorma
                    volumes:
                      - .:/metanorma
                    command: metanorma site generate --agree-to-terms