Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down Expand Up @@ -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()),
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
include CITATION.cff
include assets/reader-banner.png
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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/)
Expand All @@ -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:
Expand Down
Binary file added assets/reader-banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions docs/guides/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<version>` 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.
4 changes: 2 additions & 2 deletions src/reader_workbench/tests/repo/test_docs_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions src/reader_workbench/tests/repo/test_package_metadata.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import re
import tomllib
from pathlib import Path

Expand Down Expand Up @@ -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
Loading