Automated metadata management for markdown documentation repositories.
versioned-md is a CLI tool that bootstraps documentation repositories with automated metadata handling, version tracking, and review governance.
versioned-md enforces versioning on individual markdown documents. The version is a human readable integer but is also tightly coupled to the git commit hash. Additionally the change process for documents is completely piggy-backing on the review process for a pull request in GitHub.
In short, versioned-md enables a few things in addition to plain markdown documents:
- Versioning of individual documents: Each document has its own version id, tightly linked to a commit hash.
- Reviewer tracking: Know who approved each document
- Automated governance: Enforce metadata consistency without manual effort
- Stable identifiers: Unique IDs that survive renames and refactoring
The backbone of versioned-md is GitHub Actions workflows and Python tooling to handle all of that automatically.
Once everything is up and running, for the most common case — updating one or more documents — the workflow is as simple as:
- Edit one or more markdown files in
docs/ - Open a PR
- Get it reviewed using standard GitHub PR reviews
- Merge — Once merged, CI automatically bumps the version, records who changed what, tracks reviewers, and links to the PR
That's it. No special commands, no manual metadata edits, no separate versioning system to learn.
For other use-cases - adding new documents, importing history, register a new person, publish a draft document or setting up a new repository - the CLI tool is a useful tool. The CLI has subcommands:
- doc Create, promote (draft -> strict), retire or import (from a different git repository) documents
- create Setting up a documentation repository from scratch
- sync Get the latest changes in upstream CI scripts
- people Add, deactivate or import team members
- meta Validate a meta.json file
Please see the Standard Operating Procedures for instructions for specific use cases.
- Authors write Markdown docs in
docs/{strict,drafts,reference}/— metadata lives in companion.meta.jsonfiles - Open a PR — the
check-header.ymlworkflow validates that protected metadata isn't tampered with - Review and merge the PR
- Merge triggers updates — the
update-metadata.ymlworkflow runs on every merge to:- Bump the version number
- Record who updated the document and when
- Store approved reviewers
- Track the PR number
- Maintain
version_historyin companion.meta.jsonfiles
- Static output — any MkDocs, Starlight, or custom tooling can consume the
.meta.jsonfiles at will
| Category | Directory | Governance |
|---|---|---|
strict |
docs/strict/ |
Full governance. Unique 4-digit IDs. Filename must be <documentId>.md (e.g., 1001.md) |
drafts |
docs/drafts/ |
Transitional. Lightweight governance. Descriptive filenames OK |
reference |
docs/reference/ |
Static reference docs. Descriptive IDs and names OK. Still versioned by CI |
Documents can be promoted from drafts → strict via a dedicated PR. The CI handles the rest.
Install the tool globally using the uv package manager:
uv tool install git+https://github.com/your-org/versioned-md.gitThis makes versioned-md available globally — you can run it from any directory to create and manage documentation repositories.
Run versioned-md create without arguments for an interactive prompt, or supply flags for non-interactive use:
# Non-interactive: supply flags directly
versioned-md create \
--name my-docs \
--description "Team documentation repository" \
--author "Acme Corp"
# Author is used in the LICENSE file; org defaults to author if not specified
versioned-md create \
--name my-docs \
--author "Jane Doe" \
--org "Acme Corp"
# Interactive: run without flags
versioned-md createThis bootstraps a new repo with:
docs/strict/,docs/drafts/,docs/reference/directories- A
TEMPLATEbranch containing CI workflows, scripts, and Python utilities - A
mainbranch ready for documentation
From inside your new repository:
# Create your first document as a draft
versioned-md doc create \
--title "Hello World" \
--category draft \
--description "The first document"
# Add more people to your team
versioned-md people add --name "John Smith" --handle "john" --initials "JS"
# Promote a draft to strict
versioned-md doc promote docs/drafts/hello-world.md --category strict
# Push to GitHub
git remote add origin git@github.com:your-org/my-docs.git
git push -u origin mainThe TEMPLATE branch is synced automatically with versioned-md sync when CI workflows need updating.
# Add a person to your repo
versioned-md people add --name "Name" --handle "handle" --initials "XX"
# Import people from GitHub contributors + git log
versioned-md people import --dry-run --token "$GITHUB_TOKEN"# Create a new document (requires --category, drafts need a title)
versioned-md doc create --title "Doc Title" --category draft --description "Description"
# Create a reference document (uses descriptive filename, no governance)
versioned-md doc create --title "API Reference" --category reference --description "API documentation"
# Promote a draft to strict
versioned-md doc promote --path docs/drafts/my-doc.md --category strict
# Retire a document
versioned-md doc retire --path docs/strict/1001.md --reason "Replaced by 1020"
# Import an existing Markdown file
versioned-md doc import --source existing-file.md --category draft
# Import as a reference document
versioned-md doc import --source existing-file.md --category reference# Validate all documents
versioned-md meta validate
# Validate a specific file
versioned-md meta validate --path docs/strict/1001.md# Sync the TEMPLATE branch with the latest CI workflows
versioned-md syncThe typical workflow for managing documents:
# 1. Create a draft document (auto-assigns documentId)
versioned-md doc create --title "My Feature" --category draft
# 2. Promote to strict
versioned-md doc promote docs/drafts/my-feature.md --category strict
# 3. Retire a document (moves to docs/retired/)
versioned-md doc retire docs/strict/1001-old-doc.md --reason "Replaced by 1020"| Command | Purpose | Details |
|---|---|---|
doc create |
Create a new document | Prompts for category; drafts/get auto-assigned docId, strict asks for a number, reference uses descriptive id |
doc promote |
Move draft → strict | Renames file, updates category, validates documentId uniqueness |
doc retire |
Retire a document | Moves to docs/retired/, sets status: retired in .meta.json |
doc import |
Import existing Markdown file | Reads the markdown body, enriches with git history, auto-imports version_history from source .meta.json, supports --dry-run |
Each document has a companion .meta.json file that tracks version_history — a log of all changes made to the document.
# Validate .meta.json files against the schema
versioned-md meta validate # check all docs in the repo
versioned-md meta validate -p docs/strict/1001.meta.json # specific file
versioned-md meta validate -p docs/strict/1001.md # companion file (auto-discovers .meta.json)The version_history array in each .meta.json is validated by CI:
- Schema enforcement ensures required fields (
version,updated_by,last_updated) - All fields on existing entries are locked (deep equality check on every field)
- First PRs can include imported history from other repositories
- Post-merge, CI appends entries automatically (skips if already present)
{
"version_history": [
{
"version": "1",
"updated_by": "jane",
"last_updated": "2024-03-20",
"reviewer": ["john", "bob"],
"commit_hash": "abc1234",
"pr_number": 42,
"action": "created"
}
]
}The people.json file is a registry of everyone who is allowed to author or review documentation in the repository. Before a PR can be merged, CI checks that:
- The PR author is listed in
people.json(to ensure only known individuals contribute) - Every reviewer who approved the PR is listed in
people.json(to gate approvals)
When CI detects an unknown author or reviewer, it blocks the PR. Adding people to the registry is therefore a prerequisite to any document workflow.
# Non-interactive: supply all fields via flags
versioned-md people add --name "Jane Doe" --handle "jane" --initials "JD"
# Interactive: run without flags in a terminal
versioned-md people add
# Deactivate a team member
versioned-md people deactivate --handle "jane"
# Import people from GitHub API + git log
versioned-md people import # auto-discovers from contributors & git log
versioned-md people import --dry-run # preview without writingpeople import scans the local git log and (on GitHub repos) the contributors API and PR reviews.
You'll need a GITHUB_TOKEN or --token to access the GitHub API for full discovery.
Not a GitHub repo — the command falls back to git log only.
You'll need at least one person in people.json to create documents. Use versioned-md people add or versioned-md people import to populate it.
All document metadata is stored in a companion .meta.json file alongside each Markdown file. The Markdown body contains no frontmatter — .meta.json is the single source of truth.
{
"title": "System Architecture",
"description": "A document describing the system architecture",
"category": "strict",
"documentId": "1001",
"responsible": "jane",
"status": "active",
"version": "1",
"lastUpdated": "2026-06-25",
"updatedBy": "jane",
"reviewer": ["sarah", "mike"],
"commitHash": "a1b2c3d",
"prNumber": "42",
"version_history": [...]
}| Type | Fields | Who Changes |
|---|---|---|
| Mutable | title, description, responsible |
Authors in PRs |
| Protected | category, documentId, status, version, lastUpdated, updatedBy, reviewer, commitHash, prNumber |
CI only |
The responsible field tracks the person owning the document. It is user-mutable and independent of git committer metadata.
categorymust match the document's parent directory (strict,draft,reference, orretired)documentIdmust be a unique 4-digit number forstrictanddraftcategoriesstrictfilenames must equal thedocumentId(e.g.,1001.md)version_historyentries are immutable once written; only appending new entries is allowedreferencedocuments use descriptivedocumentIds and filenames, but are otherwise versioned by CI like other documents- Protected top-level fields cannot be changed in a PR
- Mutable fields (
title,description,responsible) can only change if the Markdown body also changed
Use versioned-md doc import to bring existing Markdown files into the versioned-md structure. By default, if the source file has a companion .meta.json, its version_history is automatically merged:
# Full import with version_history
versioned-md doc import -s my-file.md -c draft
# Skip version_history merging
versioned-md doc import -s my-file.md -c draft --skip-historyThe --skip-history flag disables automatic version_history merging from the source .meta.json, useful when importing documents that already exist in the target repo.
Step-by-step guides for common documentation workflows. These ship with every repository created by versioned-md create as reference documents in docs/reference/.
- Using versioned-md — Create a repo from scratch, bootstrap with people and first document
- Creating Documents — Drafts, strict documents, and reference docs
- Updating Documents — Promote drafts to strict, retire outdated docs, import from elsewhere
- Team Management — Add people, bulk import from GitHub, deactivate team members
- CI & Governance — The PR workflow, CI checks, and what happens on merge
- Maintenance & Admin — Update CI templates, validate metadata, migrate legacy repos
- Reference — Field reference (mutable vs protected), version history rules, troubleshooting
You can also view these from your own repo after running versioned-md create:
# View the SOPs in your local docs directory
ls docs/reference/uv sync # install runtime deps + package
uv pip install -e ".[dev]" # add dev deps
uv run ruff check . # lint
uv run ruff format . # formatMIT License — see LICENSE for details.