From af1a9990b1e14cc68a61f75b65834ebd6d513a7f Mon Sep 17 00:00:00 2001 From: Eric South Date: Tue, 6 Oct 2026 22:02:13 -0400 Subject: [PATCH 1/2] Add a PNG banner for package-index rendering --- assets/reader-banner.png | Bin 0 -> 32140 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/reader-banner.png diff --git a/assets/reader-banner.png b/assets/reader-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..8714a8eb0eab51eb5b82b3b47f19565b1417ca61 GIT binary patch literal 32140 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 OsjKQ-$U0|z|GxmVYuRG} literal 0 HcmV?d00001 From 4730e2078230b992a82a8a9bef9b1825dc5b990b Mon Sep 17 00:00:00 2001 From: Eric South Date: Tue, 6 Oct 2026 22:07:05 -0400 Subject: [PATCH 2/2] Bind package-page images to immutable releases and verify their bytes --- .github/workflows/release.yaml | 15 +++++++++++++++ MANIFEST.in | 1 + README.md | 7 +++---- docs/guides/releases.md | 13 +++++++++++++ .../tests/repo/test_docs_routes.py | 4 ++-- .../tests/repo/test_package_metadata.py | 19 +++++++++++++++++++ 6 files changed, 53 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4bf9dbc5..a10ee917 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -47,6 +47,20 @@ jobs: > .tmp/release-evidence/checks.json - name: Verify lockfile run: uv lock --check + - name: Verify immutable package-page banner + run: | + python - <<'PYBANNER' + import pathlib, re, tomllib, urllib.request + root = pathlib.Path('.') + project = tomllib.loads((root / 'pyproject.toml').read_text())['project'] + url = re.findall(r'!\[[^\]]*\]\(([^)]+)\)', (root / 'README.md').read_text())[0] + expected = f"https://raw.githubusercontent.com/e-south/reader/v{project['version']}/assets/reader-banner.png" + assert url == expected, 'Banner must be bound to the released version' + with urllib.request.urlopen(url, timeout=30) as response: + published = response.read(10_000_001) + assert published == (root / 'assets/reader-banner.png').read_bytes(), 'Published banner differs from source' + assert len(published) < 10_000_000 and published.startswith(b'\x89PNG\r\n\x1a\n') + PYBANNER - name: Build distributions run: | export SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" @@ -83,6 +97,7 @@ jobs: 'name': project['name'], 'version': project['version'], 'revision': os.environ['GITHUB_SHA'], 'tag': os.environ['GITHUB_REF_NAME'], 'python': platform.python_version(), 'uv_lock_sha256': sha(root / 'uv.lock'), + 'readme_sha256': sha(root / 'README.md'), 'banner_sha256': sha(root / 'assets/reader-banner.png'), 'uv': subprocess.check_output(['uv', '--version'], text=True).strip(), 'build_lock_sha256': sha(evidence / 'build-requirements.lock'), 'checks': json.loads((evidence / 'checks.json').read_text()), diff --git a/MANIFEST.in b/MANIFEST.in index ebf72c31..02cf9bad 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include CITATION.cff +include assets/reader-banner.png diff --git a/README.md b/README.md index 9f7e71ed..d276a12e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ![Reader data workbench](https://raw.githubusercontent.com/e-south/reader/main/assets/reader-banner.svg) +# ![Reader data workbench](https://raw.githubusercontent.com/e-south/reader/v1.0.0/assets/reader-banner.png) [![Checks](https://github.com/e-south/reader/actions/workflows/checks.yaml/badge.svg?branch=main)](https://github.com/e-south/reader/actions/workflows/checks.yaml) [![Python 3.12](https://img.shields.io/badge/python-3.12-3776AB.svg)](https://www.python.org/downloads/release/python-3120/) @@ -14,11 +14,10 @@ The distribution and import package are named `reader-workbench` and ## Install -Reader has not yet been published to PyPI. Install the current source release as -a command-line tool: +Install Reader as a command-line tool: ```bash -uv tool install git+https://github.com/e-south/reader.git +uv tool install reader-workbench ``` For development, install from a checkout: diff --git a/docs/guides/releases.md b/docs/guides/releases.md index 2691b444..d91d5047 100644 --- a/docs/guides/releases.md +++ b/docs/guides/releases.md @@ -72,3 +72,16 @@ an additional archival identifier, not a substitute for the executable version. See [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/) and the [namespace migration](./package_namespace_migration.md). + +### Package-page images + +The README is also the PyPI description. Keep its links absolute and use a +PNG banner at an immutable source commit or the matching `v` tag. +Never use a relative asset path or a mutable branch for a release image. Keep +published tags and their assets; changing a new banner must not change older +release pages. When incrementing the version, update a version-bound README +image URL in the same change. The package tests enforce this relationship. + +Before publishing, fetch the banner URL after the tag exists and compare its +bytes with the source PNG. Check the rendered PyPI page after upload. The PNG is +a package-page export; its editable SVG remains the artwork source. diff --git a/src/reader_workbench/tests/repo/test_docs_routes.py b/src/reader_workbench/tests/repo/test_docs_routes.py index bb31e671..07c2db56 100644 --- a/src/reader_workbench/tests/repo/test_docs_routes.py +++ b/src/reader_workbench/tests/repo/test_docs_routes.py @@ -14,11 +14,11 @@ def test_root_readme_is_a_human_first_landing_page() -> None: text = (REPO_ROOT / "README.md").read_text(encoding="utf-8") assert text.startswith("# ![Reader") assert not text.startswith("# reader\n") - assert "uv tool install git+https://github.com/e-south/reader.git" in text + assert "uv tool install reader-workbench" in text assert "python -m pip install reader-workbench" not in text assert "uv tool install ." not in text assert "python -m pip install ." not in text - assert "not yet been published to PyPI" in text + assert "not yet been published to PyPI" not in text assert "\nreader demo\n" in text assert "\nreader protocols\n" in text assert "uv run reader demo" in text diff --git a/src/reader_workbench/tests/repo/test_package_metadata.py b/src/reader_workbench/tests/repo/test_package_metadata.py index e5fb5dd9..a14298d1 100644 --- a/src/reader_workbench/tests/repo/test_package_metadata.py +++ b/src/reader_workbench/tests/repo/test_package_metadata.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re import tomllib from pathlib import Path @@ -56,3 +57,21 @@ def test_compile_checks_target_the_existing_import_package() -> None: assert "compileall -q src/reader_workbench" in text, path assert "compileall src/reader`" not in text, path assert "compileall -q src/reader\n" not in text, path + + +def test_package_readme_uses_durable_images_and_absolute_links() -> None: + root = REPO_ROOT + project = tomllib.loads((root / "pyproject.toml").read_text())["project"] + assert project["readme"] == "README.md" + text = (root / "README.md").read_text() + targets = re.findall(r"\]\(([^)]+)\)", text) + assert all(target.startswith(("https://", "#")) for target in targets) + images = re.findall(r"!\[[^\]]*\]\(([^)]+)\)", text) + banner = images[0] + prefix = "https://raw.githubusercontent.com/e-south/reader/" + assert banner.startswith(prefix) + revision, relative = banner.removeprefix(prefix).split("/", 1) + assert revision == "v" + project["version"] or re.fullmatch(r"[0-9a-f]{40}", revision) + assert relative.endswith(".png") + data = (root / relative).read_bytes() + assert data.startswith(b"\x89PNG\r\n\x1a\n") and len(data) < 10_000_000