Somebody needs last quarter’s synthetic release regenerated — to compare against this quarter’s, to investigate a defect a consumer reported, or because an auditor asked whether it can be done at all — and you have the manifest and nothing else.
Part of Seed Management & Run Reproducibility: that page covers how to build a pipeline whose runs can be reproduced. This one is the procedure for actually doing it, in the order that fails fastest, and the verification that turns a regenerated file into evidence.
A manifest records the state the run depended on. Reproducing it means restoring that state, and the reason a first attempt usually fails is that restoration has an order. Each of these depends on the one before it, and attempting them out of order produces a failure whose message points at the wrong thing:
The container. Everything below runs inside it. Restore it first, by digest, and never by tag.
The environment variables.PYTHONHASHSEED and the thread counts change results and appear in no dependency list, so a run that restores packages but not these fails with no visible cause.
The packages. Install the resolved list, not the requirement list, and verify the native library versions afterwards rather than trusting the wheel names.
The inputs. Fetch each asset and check its digest before use. An input that has been “corrected” since the release is the single most common reason a reproduction diverges, and it diverges silently.
The seed derivation. Replay it from the recorded root and rule; do not draw fresh streams.
Attempting step five with step four unverified produces an artifact that differs and gives no indication why. Attempting step three inside the wrong container produces a native library mismatch that presents as a geometry difference of a few micrometres, which then propagates into a completely different hash.
Restoration has an order, and each step's failure presents as a defect in a later one — which is why the checks are cheapest to run in this sequence.
Before restoring anything, verify the manifest can support a reproduction at all. A manifest missing any of these fields cannot, and finding that out now costs a minute rather than an afternoon.
Field presence against the cost of absence: the fields teams record are the ones a tutorial mentions, and the fields that block a reproduction are the ones left out.
python
import json
from pathlib import Path
REQUIRED ={"root_seed","derivation","environment","inputs","artifact",}
REQUIRED_ENV ={"packages","proj","geos","hash_seed","threads","image_digest"}defcheck_manifest(path: Path)->list[str]:
m = json.loads(path.read_text())
gaps =sorted(REQUIRED - m.keys())
gaps +=[f"environment.{k}"for k insorted(REQUIRED_ENV - m.get("environment",{}).keys())]ifnot m.get("inputs"):
gaps.append("inputs (empty)")if"sha"notin m.get("artifact",{}):
gaps.append("artifact.sha — nothing to compare a reproduction against")return gaps
gaps = check_manifest(Path("release-2026Q2/manifest.json"))print("cannot reproduce; missing:", gaps)if gaps elseprint("manifest is complete")
If artifact.sha is missing, stop. Without it there is no definition of success, and a regenerated file that looks plausible proves nothing.
# The tag in the manifest is informational. The digest is the identity.DIGEST=$(jq -r'.environment.image_digest' release-2026Q2/manifest.json)docker pull "ghcr.io/example/spatial-gen@${DIGEST}"docker run --rm-it\-v"$PWD/release-2026Q2:/work"\"ghcr.io/example/spatial-gen@${DIGEST}"bash
If the digest is no longer available in the registry, that is a finding in its own right and should be recorded: the release is no longer reproducible, and the fix is a retention policy on generation images rather than anything in the pipeline.
import os
import subprocess
import sys
env = json.loads(Path("/work/manifest.json").read_text())["environment"]
os.environ["PYTHONHASHSEED"]= env["hash_seed"]
os.environ.update({k: v for k, v in env["threads"].items()if v})
subprocess.run([sys.executable,"-m","pip","install","--no-deps",*env["packages"]], check=True)import pyproj, shapely
assert pyproj.proj_version_str == env["proj"],(f"PROJ {pyproj.proj_version_str} != recorded {env['proj']}")assert shapely.geos_version_string == env["geos"],(f"GEOS {shapely.geos_version_string} != recorded {env['geos']}")
--no-deps matters. Installing the resolved list with dependency resolution enabled lets pip re-resolve transitive requirements and quietly install something the original run did not have.
PYTHONHASHSEED has to be set before the interpreter starts to take effect, so in practice this block belongs in an entrypoint that re-executes the process — setting it from inside a running interpreter changes nothing.
deffetch_inputs(manifest:dict, dest: Path)->None:for rel, expected insorted(manifest["inputs"].items()):
path = dest / rel
path.parent.mkdir(parents=True, exist_ok=True)
fetch_from_archive(rel, path)# your object store, CAS, or backup
actual = asset_digest(path)if actual != expected:raise SystemExit(f"input drift: {rel}\n recorded {expected}\n fetched {actual}\n"f" the release cannot be reproduced from the current asset")
Raising here rather than warning is the whole point. An input that has changed since the release means the reproduction would be a different run that happens to use the same code, and reporting its hash mismatch later as a pipeline defect wastes the next several hours.
manifest = json.loads(Path("/work/manifest.json").read_text())
result = generate(
root_seed=manifest["root_seed"],
inputs=Path("/work/inputs"),
out=Path("/work/repro"),)
Nothing here re-derives anything from the clock, the hostname or the worker count. If generate accepts a seed at all it accepts this one, and if it accepts anything else — a --jobs flag that changes the partitioning, say — that flag is part of the manifest or the pipeline is not reproducible.
defverify(repro: Path, manifest:dict)->None:
actual = asset_digest(repro)
expected = manifest["artifact"]["sha"]
verdict ={"release": manifest["artifact"]["path"],"expected_sha": expected,"actual_sha": actual,"reproduced": actual == expected,}
Path("/work/reproduction-report.json").write_text(
json.dumps(verdict, indent=2, sort_keys=True))ifnot verdict["reproduced"]:raise SystemExit("artifact hash mismatch — see reproduction-report.json")
Write the report whether or not it succeeded. A failed reproduction is evidence too, and it is the input to auditing a run that cannot be reproduced; a failure that leaves no artifact behind has to be re-run before it can be investigated.
When the hashes disagree, comparing at the partition level narrows the cause logarithmically — provided the seed derivation was keyed on partition identity in the first place.
It is worth being precise about this, because a reproduction report is often quoted as though it
settled more than it does.
A matching hash proves that the recorded inputs, run through the recorded environment with the
recorded seed, produce the recorded artifact. That is exactly the claim, and it is a strong one:
it means the release is not a one-off, that nothing undeclared influenced it, and that a reviewer
can inspect the process rather than taking the output on trust.
It does not prove the release is correct. A generator with a bug reproduces its bug perfectly, and
a reproduction is silent on whether the statistics are faithful, whether the privacy budget was
computed properly, or whether the contract was the right contract. Those are the validation
gates’ job, and a release that reproduces and fails them is reproducibly wrong.
Nor does it prove the release will reproduce next year. It proves it reproduced today, in an
environment that was restorable today. The registry entry for the image digest, the retention of
the input assets and the availability of the generator version are all live dependencies, and any
one of them can lapse quietly. That is why the reproduction report is worth writing to the
registry rather than to a terminal: a record of when a release was last successfully reproduced is
the only early warning that the ability to do so has decayed.
The release predates the manifest schema. Older releases often carry a partial record — a seed and a requirements file, and nothing else. They are not reproducible, and saying so plainly is better than producing a file that “looks right”. What is worth doing is recording, once, which releases fall on which side of the line, so the question is answered from a table rather than re-investigated each time.
The reproduction succeeds and a consumer still sees a difference. The consumer is comparing something other than the artifact — a derived index, a cached tile pyramid, a database load with its own ordering. Compare hashes at the artifact boundary first, and only then walk forward through whatever transformed it.
The generator has been fixed since the release. Then reproducing the release requires the generator version the manifest names, which means the generator has to be versioned and retained like any other input. Reproducing a release with today’s generator is a useful experiment and is not a reproduction; label it accordingly or it will be quoted as one.
Reproduction spends privacy budget. It does not, provided it genuinely reproduces — the same artifact is not a new observation of the population. A “reproduction” that draws fresh noise is a second release and must be debited, which is one more reason the hash comparison is not optional.