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:
-
Running Metanorma (through shell commands)
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 |
|---|---|---|---|
Yes (native actions) |
Yes (GitHub Pages) |
Yes (per-document releases, channels, aggregation) |
|
Yes (Docker) |
Yes (GitLab Pages) |
No |
|
Yes (Docker) |
Manual |
No |
|
Yes (Docker) |
Manual |
No |
|
Yes (Docker) |
Manual |
No |
|
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 |
|---|---|---|
Installs the |
Any workflow |
|
Caches files used by and generated by Metanorma during compilation (temporary fonts, Relaton data, site output) to speed up subsequent runs. |
Any build workflow |
|
Compiles a single Metanorma document to HTML/PDF/XML/etc. |
Single-document repos |
|
Compiles all documents from a |
Any repo with multiple documents |
|
Meta-action: calls |
Simple site deployments |
|
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 |
|
Discovers compiled documents, detects changes, and publishes them as per-document GitHub Releases with channel-based routing and stage gating. |
Organization-scale publishing |
|
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 |
|
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+releasein each per-document repository, andaggregatein 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.
.github/workflows/generate.yml file for generating a Metanorma site and deploying it to GitHub Pagesname: 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 generatewhich 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@v1to compile documents, then runsactions-mn/release@v1to publish changed documents as GitHub Releases. actions-mn/.github/.github/workflows/metanorma-generate.yml-
Checks out the repo inside the
metanorma/metanormaDocker container, runsactions-mn/cache@v1andactions-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 |
|---|---|---|
|
Compiles AsciiDoc sources to HTML/PDF/XML/RXL |
Per-document repo (CI) |
|
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 |
|
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, orinternal— 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 |
|---|---|---|
|
|
Comma-separated GitHub organizations to scan for repos |
|
|
Repository topic for auto-discovery |
|
|
Explicit repo list ( |
|
|
Comma-separated channels to include. Empty = all. |
|
|
Comma-separated stages to include. Empty = all. |
|
|
Directory for extracted document files |
|
|
Strip edition suffixes from filenames |
|
|
Max parallel repo processing |
|
|
Directory for persistent cache (ETags, delta state) |
|
|
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 containing the metanorma configuration |
|
|
Output directory containing compiled documents |
|
|
Release manifest file |
|
|
Default visibility for unlisted documents ( |
|
|
Force release even if content hash matches |
|
|
Comma-separated doc IDs or glob patterns to force-replace (deletes and recreates specific releases) |
|
|
Glob pattern to filter documents for release |
|
|
Comma-separated stages to release. Empty = all. |
|
|
Override channels for all documents. Empty = use manifest. |
|
|
Max parallel document processing |
|
|
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.
.gitlab-ci.yml file for generating a Metanorma site and deploying it to GitLab Pages through workflow inclusioninclude:
- 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.
.gitlab-ci.yml file for generating a Metanorma site and deploying it to GitLab Pagesimage:
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
.travis.yml file for generating a Metanorma sitelanguage: 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
.circleci/config.yml file for generating a Metanorma siteversion: 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
Jenkinsfile for generating a Metanorma sitepipeline {
agent {
docker {
image 'metanorma/metanorma'
}
}
stages {
stage('Build') {
steps {
sh 'metanorma site generate --agree-to-terms'
}
}
}
}
Bitrise
bitrise.yml file for generating a Metanorma siteformat_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