BLOG/TEAM CI/CD
THEME
TEAM CI/CD

Reproducible modules with the lockfile

A Git tag can be force-moved to different bytes tomorrow. orchstep.lock pins each remote module to an immutable commit and content hash, so module verify in CI fails on supply-chain drift instead of running it.

Jul 4, 2026 OrchStep Team 7 minROLE: DevOps EngineerSCALE: Any
RUNNABLE DEMO
Full source for this post: blog/reproducible-lockfile
VIEW SOURCE

version: "^1.0.0" resolves to a tag like v1.1.0. Comforting — until you remember a tag is just a movable pointer. The module author (or someone with push access, or a compromised CI token) can force-move v1.1.0 to different bytes tomorrow, and your next build runs code you never reviewed. Your workflow file would not change. Your diff would be empty. The thing that ran would be different.

Every ecosystem that ships executable units solves this the same way: a lockfile. go.sum, package-lock.json, .terraform.lock.hcl, Cargo.lock. OrchStep's is orchstep.lock, and it turns "newest 1.x" into "exactly these bytes."

A module resolving once, then pinning to a commit and hash that CI verifies

What gets pinned

Take a workflow that imports one remote module:

orchstep.yml
name: reproducible-lockfile
desc: "Pin remote modules to an immutable commit + content hash with orchstep.lock."

modules:
  - name: greeter
    source: "github.com/orchstep/test-module-single"
    version: "^1.0.0"                                   # resolves to v1.1.0

tasks:
  # `orchstep run hello`
  hello:
    desc: "Run a remote module — pinned by orchstep.lock"
    steps:
      - name: greet
        module: greeter
        task: hello
        with:
          name: "OrchStep"

Run module lock and OrchStep resolves the constraint and writes the lock:

orchstep module lock
  greeter              pinned  v1.1.0  tag v1.1.0  commit 8638d85ce4e0

Lock file written: orchstep.lock

Here is the real orchstep.lock it produced — every field is load-bearing:

orchstep.lock
# orchstep.lock - Auto-generated, do not edit manually.
# Pins each module to an immutable commit + content hash for reproducible,
# tamper-evident builds. Written automatically on first resolve.
#   orchstep module update   refresh pins to newest allowed versions
#   orchstep module verify   re-check commit + hash (CI gate)

modules:
    greeter:
        source: github.com/orchstep/test-module-single
        version: 1.1.0
        tag: v1.1.0
        commit: 8638d85ce4e043c15de3143b3c4a9aa2b779d38e
        hash: sha256:25b552a9e02b2fef3b962b0cc0fad85630b986575b27564964450d0363ca248c
FieldWhat it isWhy it matters
sourcewhere the module came fromidentifies the import
versionthe resolved semverhuman-readable record of ^1.0.01.1.0
tagthe Git tag matchedtraceability back to the release
committhe immutable commit SHAwhat actually gets fetched — not the tag
hashsha256 over the module's filesdetects a tampered cache or a re-pointed commit

The two that make it bulletproof are commit and hash. With a lock present, OrchStep fetches the pinned commit and ignores the tag entirely — so force-moving v1.1.0 does nothing to your build. Then it recomputes the content hash and compares. If the bytes do not match, the run fails instead of executing.

Verify in CI — the gate that earns its keep

module lock writes the pins. module verify enforces them: it re-fetches every locked module at its pinned commit, recomputes the hash, and exits non-zero on any drift.

orchstep module verify
All modules verified: commit + content hash match orchstep.lock

Drop that one line into your pipeline before anything runs, and a moved tag or a tampered module turns into a red build, not a silent code-swap:

steps:
  - uses: orchstep/setup-orchstep@v1
  - run: orchstep module verify        # fails the build on supply-chain drift
  - run: orchstep run deploy

If the upstream tag is force-moved to different bytes, verify says so in plain language:

module 'greeter' failed integrity check: orchstep.lock expects sha256:25b5...
but resolved content is sha256:1c9d... (the tag may have been force-moved or
the cache tampered; run 'orchstep module update' if this change is intended)

To forbid implicit lock writes in automation entirely, set ORCHSTEP_LOCK_AUTOWRITE=false — CI then resolves against the committed lock and refuses to write a new one.

Upgrading on purpose

A lockfile is not a freeze. When you want a newer version, module update re-resolves to the newest version your constraint allows and re-pins the commit + hash:

orchstep module update greeter   # bump 'greeter' to newest allowed, re-pin
git add orchstep.lock            # review the commit + hash change in the diff

Because the change lands as a commit + hash diff in orchstep.lock, a reviewer sees exactly which bytes changed — the property a security or platform team asks for before approving an upgrade.

The three commands, in one table

CommandWhat it doesWhen
module lockresolve + write orchstep.lock; keep existing pins, fill missingonce, then commit it
module update [name...]re-resolve to newest allowed and re-pinintentional upgrades
module verifyre-fetch pinned commit, recompute hash, exit non-zero on driftevery CI run

What you gained

ConcernTag onlyWith orchstep.lock
What runs tomorrowwhatever the tag points atthe pinned commit, always
Force-moved tagsilently runs new codeverify fails the build
Tampered cacheundetectedhash mismatch caught
Upgradesinvisiblea reviewable commit + hash diff
Teammate parity"works on my branch"identical resolution from the lock

The honest boundary

Local-path modules (./mod, ../shared) are never locked — there is nothing remote to pin, and the files are already in your repo and your diff. The lockfile only earns its place once you import a remote module, which is exactly when the mutable-tag problem appears. And like any lockfile, it is only as good as your habit of committing it and gating CI on verify. Skip the gate and you have a pin nobody checks.

Where to go next

Importing a remote module in CI? Run orchstep module lock, commit the file, and add orchstep module verify to the pipeline. Three minutes now buys you a build that can't be swapped out from under you.

#MODULES#LOCKFILE#CI#SUPPLY-CHAIN
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — TEAM CI/CD