Skip to content

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.Z tag.
  • A source without ?ref= floats on the default branch — an uncontrolled upgrade on every terraform init -upgrade. The pin audit flags this.
  • Upgrade deliberately: bump the ?ref= to a newer tag, read that version's CHANGELOG, and review the terraform plan.

The audit runs in CI (and locally):

node apps/module-registry/dist/index.js --consumer-dir <your/infra/dir> --fail-on-issues true

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)

  1. Make the change in modules/azure/<module>/.
  2. Bump the module's version in modules/registry.json.
  3. Add a matching ## vX.Y.Z section to that module's CHANGELOG.md (Keep a Changelog style: ### Added / ### Changed / ### Fixed / ### Removed).
  4. 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.
  5. On merge to main, the module-release workflow creates the <module>/vX.Y.Z tag 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.