Fishwrap Image Contract
This document is the public contract for consumers of the fishwrap engine. If you depend on fishwrap, you depend on this contract — and only this contract. Anything not described here is an implementation detail that may change at any time.
If you are looking for the internal decision record of why fishwrap publishes as a container image, see ADR-001. This document is the external face of that decision.
The canonical example consumer is Daily Clamour. Fork it as a starting template.
1. The Artifact
Every fishwrap release publishes one signed OCI container image to GitHub Container Registry:
ghcr.io/maxspevack/fishwrap:<tag>
- Architectures:
linux/amd64. (arm64 is not currently published.) - Where it’s built: images are built and published exclusively by the release workflow at https://github.com/maxspevack/fishwrap/blob/main/.github/workflows/release.yml on tag pushes. The image’s source is whatever commit the corresponding git tag points at.
Every release publishes two tags:
| Tag form | Example | Mutability |
|---|---|---|
| Exact | v2.0.2 | Immutable. Recommended for production pinning. |
| Floating minor | v2.0 | Republished on every patch within the minor line. Useful for development. Do not pin production deploys to floating tags — see §6 (Pinning). |
Pre-release tags (-rc1, -alpha.1, etc.) publish only the exact tag and do not update floating tags.
2. Inputs — What You Mount In
The image expects two mount points:
/cfg (required)
Your configuration directory. The engine looks for these files inside it:
| Path | Required | Purpose |
|---|---|---|
/cfg/config.py | Yes | Editorial configuration. See docs/CONFIG_SCHEMA.md for the schema. |
/cfg/secrets.json | No | JSON object mapping source-specific keys to auth material (cookies, API tokens). Absent = sources requiring auth fetch in degraded mode. The engine reads only the keys it needs and ignores the rest. |
Beyond these two, anything else you place under /cfg is yours — typically your theme directory, About-page content, or other consumer-specific assets your config references.
/output (required, writable)
The directory the engine writes to. Mount this writable. The engine produces the files described in §3.
What is not part of the input contract
- Process working directory. The engine works from any CWD. Do not assume it is
/app,/cfg, or anywhere else — it is unspecified. - Environment variables. The engine reads
FISHWRAP_CONFIGinternally;entrypoint.shsets it for you when you usefishwrap-build --config <path>. Other environment variables are implementation detail. - Anything outside
/cfgand/output. The container’s filesystem layout is implementation detail.
3. Outputs — What You Get Back
After fishwrap-build completes successfully, /output contains the following files at stable paths:
| Path | Format | Stability |
|---|---|---|
/output/index.html | UTF-8 HTML (rendered Jinja2 against your theme) | Stable across patch and minor versions. |
/output/transparency_fragment.html | UTF-8 HTML fragment (the audit/transparency report block) | Stable across patch and minor versions. Designed to be embedded in an “About” or “Methodology” page. |
/output/run_sheet.json | UTF-8 JSON (machine-readable record of the published edition) | Stable across patch and minor versions. The schema may grow (additive fields), never shrink. |
/output/edition.pdf | Currently not produced in v2.0.x. PDF generation is out of scope until a future release; configs may declare LATEST_PDF_FILE but the file will not be written. Setting the path is harmless. |
What is not part of the output contract
- Exact byte counts of any output file. Output content varies based on input feeds, scoring, and timestamps.
- Filesystem permissions of output files (consumers should not depend on specific ownership beyond “writable by the user that ran the container”).
- Order of fields within
run_sheet.json(treat as an unordered JSON object). - Side-effect files inside
/cfg(e.g., the engine writes the SQLite newsroom database alongside the config if the config’sDATABASE_URLresolves there). State persistence is implementation detail; the only durable artifact is what lands in/output.
4. Invocation — How You Run It
The image exposes four documented invocations.
fishwrap-build --config <path>
Runs the full editorial pipeline (fetch → edit → enhance → print) against the config at <path>.
podman run --rm \
-v $(pwd)/my-config-dir:/cfg \
-v $(pwd)/my-output-dir:/output \
ghcr.io/maxspevack/fishwrap:v2.0.0 \
fishwrap-build --config /cfg/config.py
Exit codes:
| Code | Meaning |
|---|---|
| 0 | Pipeline completed; outputs produced. |
| 2 | Argument error (missing --config, file not found). |
| Non-zero (other) | Engine error. Stderr contains diagnostic detail. |
fishwrap-version
Prints the running image’s version to stdout, as a single line, parseable as a semver string. Nothing else — no banner, no whitespace surprises.
$ podman run --rm ghcr.io/maxspevack/fishwrap:v2.0.0 fishwrap-version
2.0.0
This is the only supported way to read the version. Do not import the Python package and read __version__; the Python module layout is implementation detail.
fishwrap-validate-config <path> (future feature)
A config-validation command is planned for a future release (tracked in #4). It will accept a config file path and exit 0 / 1 based on whether the config matches the schema documented in docs/CONFIG_SCHEMA.md. Until that lands, validate config correctness by running fishwrap-build against it and observing whether the pipeline completes; bad configs will surface as engine errors at the relevant pipeline stage.
Generic Python via --entrypoint python
For downstream scripts that need to call into the fishwrap library, override the entrypoint. The image’s Python interpreter has fishwrap on PYTHONPATH.
podman run --rm \
--entrypoint python \
-v $(pwd):/cfg \
ghcr.io/maxspevack/fishwrap:v2.0.0 \
/cfg/your-script.py
This is how Daily Clamour runs its publish_about.py glue script. The library surface inside the image (e.g., from fishwrap.db.repository import ...) is not part of the public contract — it may change between minor versions. If you find yourself relying on a specific internal API, file an issue: that is a signal we should be exposing a CLI for it instead.
5. Versioning Policy
Fishwrap follows Semantic Versioning 2.0.0 per docs/VERSIONING.md. For consumers of this image, the policy means:
| Bump type | Example | What changes | What you should do |
|---|---|---|---|
| Patch | v2.0.1 → v2.0.2 | Bug fixes only. Output is stable on identical input. | Adopt automatically (e.g., via Dependabot PRs that pass CI). |
| Minor | v2.0.x → v2.1.0 | New features. Output may change in non-breaking ways (e.g., new fields in run_sheet.json, new optional config keys). | Review release notes before adopting. Run your own validation. |
| Major | vX.x.x → v(X+1).0.0 | Breaking changes to the image contract: input/output paths, file formats, entrypoints, or invocation. | Read the migration notes. Expect to modify your config, scripts, or CI. |
Pre-release identifiers (-rc1, -alpha.1, -beta) are unstable and do not move floating tags. Treat them as preview-only.
6. Pinning Recommendation
Pin to exact tags (e.g., v2.0.2), not floating minors (e.g., v2.0).
Reasoning:
- The same git SHA in your repo should produce the same deployed image, every time. Floating tags break that property by definition — same source, different image, depending on when CI runs.
- Adopting new patches via reviewed PRs (e.g., from Dependabot) lets you see the upgrade work before it hits production, by running your own pipeline against the candidate image as a required check.
Daily Clamour’s pinning setup (Dockerfile-tracked pin + Dependabot + production-refresh as required check) is documented in its own version policy and is the canonical example.
7. Stability Guarantees
The following are part of the contract. Changes to any of them require at minimum a minor version bump (or a major bump if the change is breaking):
- The two mount points:
/cfgand/output. - The invocations described in §4 (
fishwrap-build,fishwrap-version, generic Python entrypoint).fishwrap-validate-configis a future addition tracked in #4; when it lands it will be added to this stability list. - The output file paths and formats described in §3.
- The image’s identifier (
ghcr.io/maxspevack/fishwrap). - The image running as a non-root user. (UID is currently 1000; the non-rootness is contract, the specific UID is implementation detail.)
The following are not part of the contract. They may change at any time, including in patch releases, with no notice:
- The base image (
python:3.12-slimtoday). - The Python version (3.12 today).
- The exact list of installed system packages.
- The internal filesystem layout (paths under
/usr,/opt,/app, etc.). - The
__version__attribute on thefishwrapPython package — usefishwrap-versioninstead. - The internal Python module layout (
fishwrap.fetcher,fishwrap.db.models, etc.). - The image size, layer structure, or build cache topology.
- The exact entropy of timestamps, run IDs, or other monotonic fields in output files.
8. Canonical Consumer Template
Daily Clamour is the reference implementation for “consume fishwrap as an image.” It demonstrates:
- Pinning the image version in a
Dockerfileso Dependabot can track upgrades. - Mounting a config directory at
/cfgand an output directory at/output. - Running
fishwrap-buildfrom GitHub Actions on a daily cron. - Capturing
fishwrap-versionfor downstream metadata injection (no Python imports of fishwrap). - Output validation as a deploy gate.
- Secrets travel via GitHub Actions secrets, written to a temp file and mounted at
/cfg/secrets.json.
To start your own fishwrap-powered publication, fork the Daily Clamour repository and replace the config, theme, and content. You should not need to read fishwrap source code at any point.
9. Cross-References
- ADR-001 — Release Artifact Contract — internal decision record this contract derives from
docs/VERSIONING.md— SemVer policydocs/CONFIG_SCHEMA.md— full config schema (companion document)docs/RELEASING.md— release process used to produce these images- Daily Clamour — canonical consumer
- GitHub Releases — release notes for each version