Skip to content

Repository files navigation

Metanorma Release Action

A GitHub Action that publishes compiled Metanorma documents as per-document GitHub Releases.

Part of the actions-mn ecosystem — alongside site-gen, compile, and build-and-publish.

Note: This action does not compile documents. Use site-gen for compilation first, then run this action on the compiled output.

Features

  • Per-document releases: Each document gets its own tag, asset, and GitHub Release
  • Channel-based publication: Route documents to specific portals via audience/category channels
  • Pattern-based manifests: Auto-assign channels by document ID pattern — zero per-document config
  • Stage gating: Restrict releases to specific stages (e.g. only published documents)
  • Content-hash change detection: Only re-releases documents whose compiled output actually changed
  • Immutable published releases: Published tags are created once; draft tags are updated in-place
  • Selective force-replace: Re-release specific documents without affecting others in the same repo
  • Universal flavor support: Data-driven tag naming from RXL metadata — works with all Metanorma flavors
  • Parallel processing: Fault-tolerant Promise.allSettled — one failure doesn't block others
  • TypeScript: Written in TypeScript with full type safety and 80%+ test coverage

Quick Start

# .github/workflows/release.ymlname: Releaseon:
push:
branches: [main]jobs:
release:
runs-on: ubuntu-latestpermissions:
contents: writesteps:
- uses: actions/checkout@v4
- uses: actions-mn/site-gen@v1 # compile first
- uses: actions-mn/release@v1 # then releasewith:
token: ${{ secrets.GITHUB_TOKEN }}

How It Works

  1. Discovers compiled documents from their RXL metadata files
  2. Filters by visibility (from metanorma.release.yml) and include pattern
  3. Detects changes (content hash vs. last release)
  4. Packages each changed document as a zip
  5. Publishes as per-document GitHub Releases

Downstream portals discover participating repos via the metanorma-release GitHub topic and aggregate released artifacts — no submodules, no compilation needed on the index side.

Inputs

InputDescriptionRequiredDefault
source-pathSource path containing the metanorma configurationNo.
output-dirOutput directory containing compiled documentsNo_site
release-configRelease manifest fileNometanorma.release.yml
default-visibilityDefault visibility for unlisted documents (public, private, members)Nopublic
forceForce release even if content hash matches last releaseNofalse
force-replaceComma-separated doc IDs or glob patterns to force-replaceNo''
include-patternGlob pattern to filter documents for release (e.g. cc-*)No*
stagesComma-separated stages to release. Empty = all.No''
channelsOverride channels for all documents. Empty = use manifest.No''
concurrencyMax parallel document processingNo4
tokenGitHub token for creating releasesNo${{ github.token }}

Outputs

OutputDescription
released-documentsJSON array of released document identifiers
skipped-documentsJSON array of skipped document identifiers (unchanged)
failed-documentsJSON array of failed document identifiers
total-documentsTotal number of documents processed
released-artifactsJSON array of { id, tag, url, channels } for released documents

Usage Examples

Multi-document repo with visibility control

Add a metanorma.release.yml to control which documents are publicly released:

# metanorma.release.ymldocuments:
- source: sources/cc-51015.adoc
- source: sources/cc-51024.adoc
- source: sources/cc-51026.adocvisibility: private # not ready for public release
# .github/workflows/release.ymlname: Releaseon:
push:
branches: [main]paths: ['sources/**', 'metanorma.yml', 'metanorma.release.yml']jobs:
release:
runs-on: ubuntu-latestpermissions:
contents: writesteps:
- uses: actions/checkout@v4with:
fetch-depth: 0
- uses: actions-mn/site-gen@v1
- uses: actions-mn/release@v1with:
token: ${{ secrets.GITHUB_TOKEN }}

Explicit release via tag signal

Push a release/* tag to trigger a forced release:

# .github/workflows/release.ymlon:
push:
tags: ['release/**']jobs:
release:
runs-on: ubuntu-latestpermissions:
contents: writesteps:
- uses: actions/checkout@v4
- uses: actions-mn/site-gen@v1
- uses: actions-mn/release@v1with:
force: truetoken: ${{ secrets.GITHUB_TOKEN }}

Naming Strategy Design

Tag and asset naming is determined by naming behavior, not publisher identity. Strategies are named after what they DO (how they format tags), not WHO they're for (which SDO published the document).

Why not publisher-specific strategies?

Publisher-based naming (IeeeNamingStrategy, IhoNamingStrategy, OgcNamingStrategy) is wrong for several reasons:

  1. Publisher ≠ naming convention. The naming convention is determined by the identifier format and edition format, not the publisher's identity. IHO and OGC use identical version-based naming — giving them separate classes duplicates identical behavior.

  2. One publisher, multiple conventions. IEEE uses DraftSuffixNamingStrategy for draft identifiers (extracting -d{N}) but falls back to EditionNamingStrategy for published documents. A single "IEEE strategy" would conflate two behaviors into one class.

  3. Wrong abstraction level. The strategy pattern should abstract over the WHAT (how to format a tag), not the WHO (which publisher). EditionNamingStrategy describes behavior. IeeeNamingStrategy describes an organization.

  4. Unnecessary enum proliferation. Most publishers (ISO, IEC, ITU, BIPM, OIML, UN, CSA, etc.) all use edition-based naming. Creating DocumentType.Iso, DocumentType.Iec, etc. purely to dispatch to the same strategy adds complexity without value.

Strategy Behavior Table

StrategyTag formatAsset formatUsed by
EditionNamingStrategy{id}/ed{N}[-{stage}]{id}-ed{N}[-{stage}].zipCC, ISO, IEC, ITU, BIPM, OIML, UN, CSA, M3AAWG, MPFA, PDFA, Ribose, unknown
VersionNamingStrategy{id}/v{N}{id}-v{N}.zipIHO, OGC
InternetDraftNamingStrategyid-{name}/{draftN}draft-ietf-{name}-{draftN}.zipIETF Internet-Drafts
RfcNamingStrategy{id}/ed{N}{id}.zipIETF RFCs
DraftSuffixNamingStrategy{base}/{N} (from -d{N} suffix){id}.zipIEEE Drafts

Dispatch

DocumentType (detected from identifier prefix) maps to a naming strategy in createDefaultRegistry(). Multiple DocumentType values can map to the same strategy instance — e.g., Iho and Ogc share one VersionNamingStrategy.

Release Tag Convention

Each document gets its own release tag and asset, independent of other documents in the same repo. Tag naming is data-driven from RXL metadata — normalized docidentifier + edition + stage — so it works for any Metanorma flavor without special-casing.

DocumentStageTagAssetStrategy
CC standardPublishedcc-51015/ed1cc-51015-ed1.zipEdition
CC standardWorking Draftcc-51015/ed2-wdcc-51015-ed2-wd.zipEdition
ISOPublishediso-8601-1-2019/ed1iso-8601-1-2019-ed1.zipEdition
ISOWDiso-wd-8601-1-2026/ed2-wdiso-wd-8601-1-2026-ed2-wd.zipEdition
IETF I-Did-calext-jscalendar/32draft-ietf-calext-jscalendar-32.zipInternetDraft
IETF RFCPublishedrfc-8984/ed1rfc-8984.zipRfc
IEEE Draftieee-draft-std-987-6-2020/3ieee-draft-std-987-6-2020-d3.zipDraftSuffix
IHOPublisheds-102/v2.1.0s-102-v2.1.0.zipVersion
OGCPublished17-069r3/v1.017-069r3-v1.0.zipVersion

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.

Release Manifest

The metanorma.release.yml file controls which documents in a repo are eligible for release, their channels, and stage constraints. If this file is absent, all documents are released.

Pattern-based channel assignment

Use pattern to auto-assign channels by document ID:

# metanorma.release.ymldocuments:
- 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 like cc-s-51020, the pattern cc-s-* automatically assigns it to public/standards. No manifest update needed.

Exact source matching

For single-document repos or exceptions, use source:

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

Stage gating

Restrict releases to specific stages:

documents:
- pattern: "cc-s-*"stages: [published] # only published stage creates a releasechannels: [public/standards]

Working drafts and committee drafts never create a GitHub Release with this constraint.

Visibility

ValueEffect
public (default)Document is packaged and released
privateDocument is not released publicly
membersReserved for future use (member-only access)

When a manifest exists but a document doesn't match any pattern or source, it defaults to the default-visibility input (default: public).

Change Detection

The action uses content hashing to avoid re-releasing unchanged documents:

  1. SHA-256 hash all files in each document's output directory
  2. Compare against the hash stored in the previous GitHub Release body
  3. If the hash matches → skip (unchanged)
  4. If the hash differs → package and release

The hash is stored in the first line of the release body: content-hash:{sha256hex}.

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 (downstream portal) filters by it. A portal cannot override or discover channels the publisher didn't assign.

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@v1with:
force-replace: 'cc-s-51015'# exact doc ID# or: force-replace: 'cc-s-*' # glob patterntoken: ${{ secrets.GITHUB_TOKEN }}

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

Release metadata

Each GitHub Release carries structured metadata in its body for downstream consumers:

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 mn-release-metadata JSON block (inside an HTML comment) is parsed by actions-mn/aggregate for channel filtering and indexing.

Discovery via GitHub Topics

Add the metanorma-release topic to your repository to opt in to portal discovery:

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

Portals discover participating repositories:

curl "https://api.github.com/search/repositories?q=topic:metanorma-release+org:CalConnect"

Architecture

The action follows a pipeline pattern with pluggable interfaces at each stage:

Discover → Filter → Detect → Package → Publish
(RXL) (manifest) (hash) (zip) (GitHub)

Each stage is defined by an interface (IDocumentExtractor, IVisibilityFilter, IChangeDetector, IArtifactPackager, IReleasePublisher), making the pipeline extensible without modifying existing code.

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

Publish a Metanorma document on a version tag

Resources

Stars

1 star

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages