Module Versioning & Private Registry (F11)
How SnowOps modules are versioned, published, and consumed. This is the consumer pin strategy and the maintainer release process in one place.
The registry
The "private Terraform registry" is the monorepo itself. There is no hosted
registry service — modules are published as git tags, one tag prefix per
module, and consumers pin a tag in their source. This is the Terraform
git module source
mechanism, which needs no infrastructure and works with private GitHub repos via
the consumer's existing credentials.
Source of truth: modules/registry.json — the
manifest listing every published module, its path, and its current semver
version. The apps/module-registry tool validates
it, builds the published index, plans releases, and audits consumer pins.
| Concept | Value |
|---|---|
| Tag format | <module>/v<version> — e.g. baseline/v1.0.0 |
| Source format | git::https://github.com/snowops/snowops-automation.git//<path>?ref=<module>/v<version> |
| Version scheme | strict semver X.Y.Z (no ranges, no pre-release) |
| History | one CHANGELOG.md per module, top entry == manifest version |
Consuming a module (the pin strategy)
Always pin a published tag. Never float on a branch.
module "baseline" {
source = "git::https://github.com/snowops/snowops-automation.git//modules/azure/baseline?ref=baseline/v1.0.0"
# ...inputs...
}
- The
?ref=is mandatory and must be a published<module>/vX.Y.Ztag. - A
sourcewithout?ref=floats on the default branch — an uncontrolled upgrade on everyterraform init -upgrade. The pin audit flags this. - Upgrade deliberately: bump the
?ref=to a newer tag, read that version's CHANGELOG, and review theterraform plan.
The audit runs in CI (and locally):
It flags unpinned sources, ref-mismatch (the ref's prefix doesn't match the
module), and unknown-version (a ref pointing at a version that was never
published). See examples/consumer-pinned/ (passes) and
examples/consumer-unpinned/ (fails) in the tool.
Semver policy
| Bump | When |
|---|---|
major (X) |
breaking input/output change, or behavior that forces resource replacement |
minor (Y) |
new optional input/output or capability, backward-compatible |
patch (Z) |
bug fix / internal change, no interface change |
Pre-v1.0.0 (e.g. F0 contracts at 0.1.0), any field may change between
minors — call it out in the CHANGELOG.
Releasing a module (maintainer)
- Make the change in
modules/azure/<module>/. - Bump the module's
versioninmodules/registry.json. - Add a matching
## vX.Y.Zsection to that module'sCHANGELOG.md(Keep a Changelog style:### Added/### Changed/### Fixed/### Removed). - Open a PR. CI runs the registry validation — the CHANGELOG top version must equal the manifest version, semver must be valid, and a version may never drop below an already-published tag.
- On merge to
main, themodule-releaseworkflow creates the<module>/vX.Y.Ztag and a GitHub Release whose body is that CHANGELOG section. Modules already tagged at their manifest version are skipped (idempotent).
Brownfield adoption (existing modules)
Modules that predate the registry are adopted by adding a row to the manifest +
a CHANGELOG.md at the version they already ship at — no code change. The
initial F11 drop seeded the 10 core published modules (F0 contracts at 0.1.0;
F1–F6 + J1/J2/J6 at 1.0.0). To adopt another module later, add its manifest row
and a ## v1.0.0 CHANGELOG entry; the validator and release workflow pick it up
automatically.
Removal path
The registry is additive metadata. To back F11 out entirely: delete
modules/registry.json, the per-module CHANGELOG.md files, apps/module-registry/,
and .github/workflows/module-release.yml. Already-created git tags can remain
(harmless) or be deleted with git push --delete origin <tag>. No cloud
resources are involved.