Created 2026-05-20. Addresses World-Threat-Model RedTeam attack A10 (Zenodo / arXiv / DOI vendor concentration) at low cost.
The current publishing pipeline targets:
- Zenodo (CERN-backed) — primary archive, DOI assignment
- arXiv (Cornell) — scholarly entry, preprint tracking
- GitHub — source code + template diffusion
These institutions have been stable for 15–20 years and are likely safe through the next decade. But the World Threat Model run flagged that on the multi-decade horizon the template claims (30–50 years), even CERN/Cornell policy changes are non-zero, and a template that puts all archival eggs in those three baskets is over-concentrated.
This guide documents two cheap parallel archival targets that buy genuine multi-decade insurance: IPFS (or content-addressed equivalents) and Software Heritage.
For any artifact that matters multi-decadally (published manuscript PDF + Zenodo deposit + source-code snapshot at publication), pin it to at least 3 of the following independent archival systems:
| System | What it preserves | Permanence horizon | Vendor risk |
|---|---|---|---|
| Zenodo (CERN) | Files + metadata + DOI | 50+ years (institutional commitment) | Single-institution policy risk |
| arXiv (Cornell) | Preprint + LaTeX source | 30+ years | Single-institution policy risk |
| GitHub (Microsoft) | Source code + history | Until Microsoft says otherwise | High brand-vendor risk |
| Software Heritage | Source code (cross-host harvested) | 100+ year mission, library-funded | Low — French gov + UNESCO partnership |
| IPFS pin (Pinata, Filecoin, Web3.Storage) | Files by content hash | Depends on pinning service | Distributed; needs at least 2 pins |
| Internet Archive | Web pages, PDFs | 30+ years; mission-aligned | Single-org policy risk |
The strategy is redundancy across independent failure domains, not picking a single winner. Pick 3 from the above for every multi-decade-relevant artifact.
For each publication:
- Primary: Zenodo deposit with DOI (already in
infrastructure/publishing/) - Source-code mirror: Software Heritage automatically harvests from GitHub; verify after each release at
https://archive.softwareheritage.org/browse/origin/?origin_url=<repo_url> - Content-addressed redundancy: two independent IPFS pins (e.g., Pinata + Web3.Storage), with the CIDs (content identifiers) recorded in the Zenodo metadata
A "publication" in this context means a manuscript + its reproducibility bundle. The archived bundle should include:
- The rendered PDF (Stage 5 output) — the human-readable canonical artifact
- The Markdown manuscript sources (
projects/<name>/manuscript/) — durable plain-text - A frozen lockfile (
uv.locksnapshot at publication commit) - The source-code commit hash (linked to the GitHub commit + Software Heritage snapshot)
- A
MANIFEST.jsonlisting all files + their SHA-256 hashes - A
PROVENANCE.jsonwith build environment metadata (Python version, OS, deterministic seed values used)
The current publishing pipeline produces some of these; the rest are the gap this guide is addressing.
The code lives at infrastructure/publishing/archival/ (providers.py,
orchestrate.py, models.py; driven by the opt-in Stage 13 wrapper
scripts/runner/archive_publication.py). The public entry point is
archive_publication():
def archive_publication(
bundle: Path,
*,
providers: list[ArchivalProvider],
dry_run: bool = True,
output_receipts_path: Path | None = None,
) -> ArchivalRun:
"""Mirror a publication bundle to N independent archival targets.
dry_run defaults to True; pass dry_run=False to perform real deposits.
Never raises on a per-provider failure — failures surface as
status="error" receipts inside the returned ArchivalRun. Each receipt
records the target name, the URL / DOI / CID / SWHID of the deposited
artifact, a timestamp, and any partial-failure mode.
"""Concrete provider classes (ZenodoProvider, IPFSPinataProvider,
IPFSWeb3StorageProvider, SoftwareHeritageProvider) implement the
ArchivalProvider protocol; load_credentials() resolves their tokens.
The function:
- Validates the bundle is complete (PDF, sources, lockfile, manifest)
- For each target, calls the target-specific deposit API (Zenodo REST API, IPFS HTTP API, SWH save-code-now API)
- Records every CID/DOI/SWHID in a receipts JSON written to the caller-supplied
output_receipts_path(recommended: commit it alongside the publication) - Updates
STATUS.mdwith the most recent successful archival run
- Zenodo API token: keep in
~/.zenodo/credentials(per-user, not in repo) - Pinata JWT: per-user, not in repo
- Web3.Storage token: per-user, not in repo
- Software Heritage: no token needed for save-code-now; uses GitHub URL
load_credentials() reads credentials from environment variables first
(ZENODO_API_TOKEN, PINATA_JWT, WEB3_STORAGE_TOKEN), then falls back to a
per-user JSON file at ~/.config/template-archival/credentials.json — never
from any in-repo file. If credentials are missing, each provider emits a
structured per-target receipt indicating it was unreachable rather than raising.
After each publication run:
- Zenodo: visit DOI URL, confirm files are listed and downloadable
- Software Heritage: visit
https://archive.softwareheritage.org/browse/origin/?origin_url=<repo>and confirm the latest commit is harvested - IPFS:
ipfs cat <CID>from a fresh node should return the file; alternatively use a public gateway likehttps://ipfs.io/ipfs/<CID> - Manifest: every file in
MANIFEST.jsonmust be retrievable from at least 2 independent targets
The verification step is part of STATUS.md row "Publishing" — refresh quarterly.
| Scenario | Probability (50yr) | Mitigated by |
|---|---|---|
| Zenodo discontinued / scope-changed | ~10% | Software Heritage + IPFS retain source + bundle |
| arXiv policy change makes preprints harder to deposit | ~20% | Zenodo + IPFS retain PDF |
| GitHub-the-brand replaced; URL changes | ~40% | Software Heritage retains code; IPFS retains bundle |
| All three IPFS pins lapse (no one paying) | ~70% over 50yr if not maintained | Zenodo + Software Heritage retain artifact even without IPFS |
| Single nation-state restricts access to one provider | ~50% | Multi-provider redundancy means at least one is reachable from anywhere |
- Does not guarantee 50-year file readability — that depends on format durability (PDF/A, plain text, durable LaTeX), not archival redundancy. See
docs/maintenance/stage-10-executable-bundle.mdfor the format-durability question. - Does not preserve the runnable pipeline — that requires container snapshots, see
stage-10-executable-bundle.md. - Does not preserve external datasets — if your data lives at a third-party URL, archive it too (deposit a copy on Zenodo, or pin to IPFS) as part of the publication bundle.
Implemented as the opt-in Stage 13 archival path. archive_publication() and
its provider classes live in infrastructure/publishing/archival/; invoke via
scripts/runner/archive_publication.py (dry-run by default — pass --commit plus
--providers for real deposits; see the CLAUDE.md quick-reference). See
MAINTAINERS.md for the infrastructure/publishing/ owner.
README.md— guide hubstage-10-executable-bundle.md— the runnable-artifact directioninfrastructure/publishing/— current publishing moduleSTATUS.md— verification cadence