| name | infrastructure-publishing |
| description | Skill for the publishing infrastructure module providing academic publishing workflows including BibTeX CLI citation generation, APA/MLA citation helper functions, DOI management, Zenodo publication, arXiv submission preparation, GitHub releases, PyPI and TestPyPI package distribution, static-site deployment to GitHub Pages / Cloudflare Pages / Netlify, multi-target long-horizon archival (IPFS Pinata, Web3.Storage, Software Heritage), a central platform registry, and publication readiness validation. Use when publishing research, generating citations, minting DOIs, distributing Python packages, deploying documentation sites, or preparing archival submissions. |
Publishing Module
Academic publishing and dissemination tools for research projects.
For the release-operator workflow, start with
docs/guides/publication-runbook.md.
It is the front door for standalone public GitHub mirrors, real Zenodo DOI
minting, new-version releases, optional mirrors, status blocks, and archival
handoff.
Platform subpackages
| Path | Use when |
|---|
infrastructure.publishing.zenodo | Zenodo deposit, upload, publish, DOI |
infrastructure.publishing.github | GitHub release + assets |
infrastructure.publishing.arxiv | arXiv tarball from manuscript tree |
infrastructure.publishing.pypi | PyPI / TestPyPI package distribution |
infrastructure.publishing.static_site | GitHub Pages, Cloudflare Pages, or Netlify deployment |
infrastructure.publishing.archival | IPFS Pinata, Web3.Storage, Software Heritage, Zenodo mirror |
infrastructure.publishing.registry | Enumerate or look up all platform adapters |
Root exports (infrastructure.publishing) and platforms.py / api.py remain for backwards compatibility.
Publication metadata
from infrastructure.publishing import PublicationMetadata, extract_publication_metadata
metadata = extract_publication_metadata([Path("manuscript/01_introduction.md")])
The publication: block in manuscript/config.yaml records durable identifiers
(doi, version_doi, version_record, github_repository, repository_url).
The publication.published_artifacts key — a {platform_name: durable_url} map —
records artifacts published outside those fields (e.g. an IPFS CID or a
HuggingFace dataset URL) and is consumed by status_report.py to mark those
platforms as published.
Citation generation
The Python API exposes BibTeX, APA, and MLA helpers. The
infrastructure.publishing.cli generate-citation command currently exposes
BibTeX only.
from infrastructure.publishing import (
generate_citation_bibtex,
generate_citation_apa,
generate_citation_mla,
)
bibtex = generate_citation_bibtex(metadata)
Zenodo publication
from infrastructure.publishing.zenodo import publish_to_zenodo, ZenodoClient, ZenodoConfig
result = publish_to_zenodo(metadata, [Path("output/paper.pdf")], access_token="...", sandbox=True)
print(result.doi)
config = ZenodoConfig(access_token="...", sandbox=True)
client = ZenodoClient(config)
dep = client.create_deposition({"title": metadata.title, "upload_type": "publication"})
client.upload_file(dep.bucket_url, Path("paper.pdf"))
doi = client.publish(dep.deposition_id)
arXiv submission
from infrastructure.publishing.arxiv import prepare_arxiv_submission
package = prepare_arxiv_submission(output_dir, metadata)
GitHub releases
from infrastructure.publishing.github import create_github_release
url = create_github_release(
"v1.0.0", "Release 1.0.0", "Notes", [Path("paper.pdf")], token, "owner/repo"
)
PyPI / TestPyPI distribution
pypi/ implements a build, check, upload pipeline via uv build and twine. dry_run=True is the default at every layer — no accidental uploads.
from infrastructure.publishing.pypi import PyPIAdapter, PyPIConfig, run_pypi_release
adapter = PyPIAdapter(PyPIConfig(test=True))
result = adapter.publish(Path("."), dry_run=False)
print(result.status, result.url)
result = run_pypi_release(
Path("."),
test=False,
dry_run=False,
verify=True,
package_name="mylib",
)
Environment: PYPI_TOKEN (production), TESTPYPI_TOKEN (test). Both resolved from os.environ when config.token is None.
Static-site deployment
static_site/ provides GitHubPagesAdapter, CloudflarePagesAdapter, and NetlifyAdapter behind a common deploy(dry_run=True) interface.
from infrastructure.publishing.static_site import (
SiteDeployConfig, SiteHosting, get_adapter,
)
config = SiteDeployConfig(
hosting=SiteHosting.GITHUB_PAGES,
site_dir="output/docs",
repo="owner/repo",
branch="gh-pages",
)
result = get_adapter(config).deploy(dry_run=False)
print(result.status, result.url)
Credentials: GITHUB_TOKEN (GitHub Pages), CLOUDFLARE_API_TOKEN (Cloudflare), NETLIFY_AUTH_TOKEN (Netlify). All adapters never raise on failure — inspect result.status and result.error.
Platform registry
registry.py is the single source of truth for all adapter metadata. It
currently records 12 first-class platforms plus documented ebook, print, retail,
and payment targets that do not yet have live adapters.
from infrastructure.publishing.registry import get_platform, list_platforms, PublishingTier
info = get_platform("pypi")
print(info.env_vars)
for p in list_platforms(tag="static_site"):
print(p.name, p.adapter_class)
README publishing-status compilation
status_report.py turns the registry (which platforms exist) plus a project's
manuscript/config.yaml (what has been published) into a regenerable README
block delimited by <!-- PUBLISHING-STATUS:START ... --> / <!-- PUBLISHING-STATUS:END -->
markers. The committed block reflects durable publication state (DOIs, repo,
published_artifacts) only — never machine-specific credential presence.
from pathlib import Path
from infrastructure.publishing.status_report import (
compile_publishing_status, render_status_block, update_readme_block,
)
report = compile_publishing_status(Path("projects/templates/template_gold_refinement"))
print(report.published_count)
block = render_status_block(report)
new_text, changed = update_readme_block(
readme_text, report, init_after_heading="## Publication and rendering"
)
A caller-supplied published={"ipfs_pinata": "https://..."} map upgrades extra
rows to PUBLISHED, overriding config. PublicationState is PUBLISHED,
AVAILABLE, or PLANNED.
uv run python -m infrastructure.publishing.status_report --project <path> --write \
--init-after "## Publication and rendering"
uv run python -m infrastructure.publishing.status_report --project <path> --check
Credential verification
credential_check.py is a read-only, non-destructive check that publishing
credentials authenticate. It sends a single GET per platform and never writes,
uploads, or prints token values. PROBES covers github, huggingface_hub,
osf, ipfs_pinata, zenodo, netlify, cloudflare_pages, gumroad,
leanpub, and stripe; pypi has no read-only endpoint and reports
no-endpoint.
from infrastructure.publishing.credential_check import check_all, format_results
results = check_all(only=["github", "zenodo"])
print(format_results(results))
uv run python -m infrastructure.publishing.credential_check [--only NAMES] [--env-file .env]
Multi-platform upload runner
upload_runner.py houses the reusable per-platform upload helpers and the batch
orchestrator (extracted from scripts/publish/upload_gold_refinement.py, now a
thin CLI). Every uploader is dry-run by default and returns a receipt dict; a
per-platform failure is captured, never aborting the batch.
from pathlib import Path
from infrastructure.publishing.upload_runner import (
UploadTargets, select_jobs, run_uploads,
)
targets = UploadTargets(
project_root=Path("."),
pdf=Path("output/paper.pdf"),
web_dir=Path("output/web"),
hf_repo_id="org/dataset",
github_repo="owner/repo",
osf_title="My Project",
)
jobs = select_jobs(include_github=False, include_static=False)
run = run_uploads(targets, jobs=jobs, commit=False)
print(run.mode, run.ok, run.results)
CORE_UPLOADERS are pinata, huggingface, osf, testpypi;
OPTIONAL_UPLOADERS (github, netlify, cloudflare) are opt-in via
include_github / include_static. commit=True performs real uploads.
Publication readiness
from infrastructure.publishing import validate_publication_readiness, create_submission_checklist
readiness = validate_publication_readiness(markdown_files, pdf_files)
checklist = create_submission_checklist(metadata)
Executable bundle (Stage 12)
from infrastructure.publishing.executable_bundle import bundle_project
manifest_path = bundle_project(project_root, output_dir)
Multi-target archival (Stage 13)
archival/ is a proper subpackage. The flat archival.py shim that used to sit at the package root has been removed; import infrastructure.publishing.archival resolves directly to the package.
from infrastructure.publishing.archival import (
ZenodoProvider,
IPFSPinataProvider,
SoftwareHeritageProvider,
archive_publication,
)
run = archive_publication(
Path("output/project/executable_bundle"),
providers=[ZenodoProvider(token=None), SoftwareHeritageProvider()],
dry_run=True,
)
CLI dry-run:
uv run python -m infrastructure.publishing.archival_cli \
--bundle output/templates/template_code_project/executable_bundle \
--providers zenodo software_heritage ipfs_pinata ipfs_web3storage
See archival/README.md and scripts/runner/archive_publication.py.