A private module registry for your org
Copy-pasted YAML is the new copy-pasted bash. Here's how to publish shared OrchStep workflows to a private registry under your own @scope and pin them with semver tags — so every team consumes the same versioned modules instead of a fork.
blog/org-module-registryOnce a few teams adopt a workflow tool, the same thing always happens: someone writes a good "deploy" workflow, three other teams copy it, and within a quarter you have four diverging versions of the same YAML. One has a fix the others don't. Another deleted the scan step. Copy-pasted YAML is just copy-pasted bash with better indentation.
The answer is the same one every language ecosystem landed on: a registry. Publish the shared workflow once, give it a version, and let teams depend on it by name and constraint instead of forking it. OrchStep does this with module registries scoped to your org.
Local for the demo, registry in production
The runnable demo uses local module folders so it works anywhere. The shape is exactly what you'd publish — a deploy-kit and a notify-kit your teams import:
name: org-module-registry
# For the demo these resolve to folders in this repo so it runs anywhere.
# In production you swap the source for a registry @scope (see below).
modules:
- name: deploy
source: "./modules/deploy-kit"
- name: notify
source: "./modules/notify-kit"
tasks:
release:
steps:
- { name: ship, module: deploy, task: rollout, with: { service: "checkout", target: "staging" } }
- { name: tell, module: notify, task: announce, with: { service: "checkout" } }The consuming workflow's whole release surface is two module: calls. It doesn't know or care how rollout works internally — it depends on the contract, the same way your app depends on a library.
Point a registry at your repo
To go from local folders to shared modules, you declare a registry @scope and pin each module with a semver constraint. The registries: block maps a scope name to the repo that holds the modules:
modules:
- name: deploy
source: "./modules/deploy-kit"
- name: notify
source: "./modules/notify-kit"registries:
acme:
url: github.com/acme/orchstep-modules
modules:
- name: deploy
source: "@acme/deploy-kit"
version: "^1.2.0"
- name: notify
source: "@acme/notify-kit"
version: "^0.4.0"Same module: calls in tasks: — only the source resolution changes. Modules live in the registry repo at modules/@acme/<name>/ and are published as tags like deploy-kit/v1.2.0. The ^1.2.0 constraint means "the highest compatible 1.x," so a deploy-kit patch reaches every team on their next run without anyone editing their workflow. More: Modules.
Versioning is the whole point
The reason copy-paste rots is that there's no version to reason about — every fork is just "the YAML, as of whenever I copied it." Constraints fix that:
modules:
- name: deploy
source: "@acme/deploy-kit"
version: "^1.2.0" # highest 1.x — gets patches, not breaking changesA team pins ^1.2.0 and gets bug fixes automatically while staying on a stable major. When deploy-kit ships a breaking v2.0.0, nobody is surprised — they opt in by bumping the constraint, on their own schedule. That's the difference between a dependency and a copy.
What you actually gained
| Concern | Copy-pasted YAML | Private registry |
|---|---|---|
| Source of truth | N forks | one tagged repo |
| Getting a fix | re-copy into each repo | ^1.2.0 picks it up |
| Breaking changes | discovered in prod | a major bump you opt into |
| Who's on what version | nobody knows | the pinned constraint |
| Ownership | diffuse | the registry repo's team |
If only one team uses a workflow, don't build a registry — keep it local. This pays off the moment a second team wants the same thing, which is exactly when copy-paste starts. Publish it once, version it, and let version: constraints do the rest.
Where to go next
- Modules — sources,
@scoperegistries, and version constraints - Composition — call module tasks as steps
- Variables & Outputs — pass inputs with
with:, read module outputs
Two teams about to copy the same workflow? Publish it under an @scope, tag it v1.0.0, and have them depend on ^1.0.0 instead of forking it.
curl -fsSL https://orchstep.dev/install.sh | sh