selenium-screenshot-compare 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- selenium_screenshot_compare-0.1.0/.github/workflows/ci.yml +110 -0
- selenium_screenshot_compare-0.1.0/.github/workflows/release.yml +77 -0
- selenium_screenshot_compare-0.1.0/.gitignore +25 -0
- selenium_screenshot_compare-0.1.0/.pre-commit-config.yaml +28 -0
- selenium_screenshot_compare-0.1.0/CLAUDE.md +123 -0
- selenium_screenshot_compare-0.1.0/CONTRIBUTING.md +65 -0
- selenium_screenshot_compare-0.1.0/PKG-INFO +288 -0
- selenium_screenshot_compare-0.1.0/README.md +257 -0
- selenium_screenshot_compare-0.1.0/pyproject.toml +57 -0
- selenium_screenshot_compare-0.1.0/requirements-dev.txt +4 -0
- selenium_screenshot_compare-0.1.0/requirements.txt +3 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare/ScreenshotCompareLibrary.py +190 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare/__init__.py +38 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare/capture.py +66 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare/comparison.py +65 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare/naming.py +17 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare/session.py +116 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare/storage.py +77 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare.egg-info/PKG-INFO +288 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare.egg-info/SOURCES.txt +26 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare.egg-info/dependency_links.txt +1 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare.egg-info/requires.txt +12 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare.egg-info/scm_file_list.json +23 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare.egg-info/scm_version.json +8 -0
- selenium_screenshot_compare-0.1.0/selenium_screenshot_compare.egg-info/top_level.txt +1 -0
- selenium_screenshot_compare-0.1.0/setup.cfg +4 -0
- selenium_screenshot_compare-0.1.0/tests/capture_auth.robot +34 -0
- selenium_screenshot_compare-0.1.0/tests/interactive_navigation.robot +53 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [dev]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint:
|
|
15
|
+
name: Lint & format (pre-commit)
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.12"
|
|
24
|
+
cache: pip
|
|
25
|
+
|
|
26
|
+
- name: Install project (dev extras)
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade pip
|
|
29
|
+
pip install -e ".[dev]"
|
|
30
|
+
|
|
31
|
+
- name: Run pre-commit on all files
|
|
32
|
+
run: pre-commit run --all-files --show-diff-on-failure
|
|
33
|
+
|
|
34
|
+
smoke:
|
|
35
|
+
name: Install & import (Python ${{ matrix.python-version }})
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
strategy:
|
|
38
|
+
fail-fast: false
|
|
39
|
+
matrix:
|
|
40
|
+
python-version: ["3.10", "3.14"]
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
|
|
44
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
45
|
+
uses: actions/setup-python@v5
|
|
46
|
+
with:
|
|
47
|
+
python-version: ${{ matrix.python-version }}
|
|
48
|
+
cache: pip
|
|
49
|
+
|
|
50
|
+
- name: Install project (robot extra)
|
|
51
|
+
run: |
|
|
52
|
+
python -m pip install --upgrade pip
|
|
53
|
+
pip install -e ".[robot]"
|
|
54
|
+
|
|
55
|
+
- name: Import package + Robot Framework library
|
|
56
|
+
run: |
|
|
57
|
+
python -c "import selenium_screenshot_compare as s; print(s.__version__)"
|
|
58
|
+
python -c "from selenium_screenshot_compare.ScreenshotCompareLibrary import ScreenshotCompareLibrary"
|
|
59
|
+
|
|
60
|
+
build:
|
|
61
|
+
name: Build package & install wheel
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@v4
|
|
65
|
+
|
|
66
|
+
- name: Set up Python
|
|
67
|
+
uses: actions/setup-python@v5
|
|
68
|
+
with:
|
|
69
|
+
python-version: "3.12"
|
|
70
|
+
cache: pip
|
|
71
|
+
|
|
72
|
+
- name: Install build backend
|
|
73
|
+
run: |
|
|
74
|
+
python -m pip install --upgrade pip
|
|
75
|
+
pip install build
|
|
76
|
+
|
|
77
|
+
- name: Build sdist and wheel
|
|
78
|
+
run: python -m build
|
|
79
|
+
|
|
80
|
+
- name: Install the built wheel and import
|
|
81
|
+
run: |
|
|
82
|
+
python -m venv /tmp/wheel-test
|
|
83
|
+
/tmp/wheel-test/bin/pip install dist/*.whl
|
|
84
|
+
/tmp/wheel-test/bin/python -c "import selenium_screenshot_compare as s; print(s.__version__)"
|
|
85
|
+
|
|
86
|
+
- name: Upload distributions
|
|
87
|
+
uses: actions/upload-artifact@v4
|
|
88
|
+
with:
|
|
89
|
+
name: dist
|
|
90
|
+
path: dist/*
|
|
91
|
+
|
|
92
|
+
robot-dryrun:
|
|
93
|
+
name: Robot Framework dry-run
|
|
94
|
+
runs-on: ubuntu-latest
|
|
95
|
+
steps:
|
|
96
|
+
- uses: actions/checkout@v4
|
|
97
|
+
|
|
98
|
+
- name: Set up Python
|
|
99
|
+
uses: actions/setup-python@v5
|
|
100
|
+
with:
|
|
101
|
+
python-version: "3.12"
|
|
102
|
+
cache: pip
|
|
103
|
+
|
|
104
|
+
- name: Install project with Robot Framework
|
|
105
|
+
run: |
|
|
106
|
+
python -m pip install --upgrade pip
|
|
107
|
+
pip install -e ".[robot]"
|
|
108
|
+
|
|
109
|
+
- name: Robot Framework dry-run
|
|
110
|
+
run: robot --dryrun --outputdir output/robot tests/
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
name: Release to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
id-token: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Build distributions
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0 # setuptools-scm needs the full history
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.12"
|
|
24
|
+
|
|
25
|
+
- name: Install build tools
|
|
26
|
+
run: pip install build
|
|
27
|
+
|
|
28
|
+
- name: Build sdist and wheel
|
|
29
|
+
run: python -m build
|
|
30
|
+
|
|
31
|
+
- name: Verify version matches tag
|
|
32
|
+
run: |
|
|
33
|
+
TAG="${GITHUB_REF#refs/tags/v}"
|
|
34
|
+
pip install --quiet dist/*.whl
|
|
35
|
+
WHEEL_VERSION=$(python -c "from importlib.metadata import version; print(version('selenium-screenshot-compare'))")
|
|
36
|
+
echo "Tag=$TAG Wheel=$WHEEL_VERSION"
|
|
37
|
+
[ "$TAG" = "$WHEEL_VERSION" ] || { echo "::error::Version mismatch"; exit 1; }
|
|
38
|
+
|
|
39
|
+
- name: Upload distributions
|
|
40
|
+
uses: actions/upload-artifact@v4
|
|
41
|
+
with:
|
|
42
|
+
name: dist
|
|
43
|
+
path: dist/*
|
|
44
|
+
|
|
45
|
+
pypi:
|
|
46
|
+
name: Publish to PyPI
|
|
47
|
+
needs: build
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
environment: pypi
|
|
50
|
+
steps:
|
|
51
|
+
- name: Download distributions
|
|
52
|
+
uses: actions/download-artifact@v4
|
|
53
|
+
with:
|
|
54
|
+
name: dist
|
|
55
|
+
path: dist/
|
|
56
|
+
|
|
57
|
+
- name: Publish to PyPI
|
|
58
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
59
|
+
|
|
60
|
+
github-release:
|
|
61
|
+
name: Create GitHub Release
|
|
62
|
+
needs: build
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
permissions:
|
|
65
|
+
contents: write
|
|
66
|
+
steps:
|
|
67
|
+
- name: Download distributions
|
|
68
|
+
uses: actions/download-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
name: dist
|
|
71
|
+
path: dist/
|
|
72
|
+
|
|
73
|
+
- name: Create release
|
|
74
|
+
uses: softprops/action-gh-release@v2
|
|
75
|
+
with:
|
|
76
|
+
generate_release_notes: true
|
|
77
|
+
files: dist/*
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python environment
|
|
2
|
+
.venv/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
.robocop_cache/
|
|
9
|
+
|
|
10
|
+
# Build artefacts
|
|
11
|
+
build/
|
|
12
|
+
dist/
|
|
13
|
+
|
|
14
|
+
# Generated outputs (captures, diffs, result.json, Robot Framework reports)
|
|
15
|
+
output/
|
|
16
|
+
|
|
17
|
+
# Downloaded Firefox binaries (heavy artefacts, ~90 MB)
|
|
18
|
+
firefoxes/
|
|
19
|
+
|
|
20
|
+
# Storage state / session secrets (cookies, tokens): DO NOT commit
|
|
21
|
+
resources/env/
|
|
22
|
+
|
|
23
|
+
# Misc OS / editors
|
|
24
|
+
.DS_Store
|
|
25
|
+
*.swp
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Hooks executed before every commit. Install with: pre-commit install
|
|
2
|
+
repos:
|
|
3
|
+
- repo: https://github.com/MarketSquare/robotframework-robocop
|
|
4
|
+
rev: v8.3.2
|
|
5
|
+
hooks:
|
|
6
|
+
# Format .robot files (ex-robotidy).
|
|
7
|
+
- id: robocop-format
|
|
8
|
+
# Lint .robot files; fails if any issue is found.
|
|
9
|
+
- id: robocop
|
|
10
|
+
|
|
11
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
12
|
+
rev: v0.6.9
|
|
13
|
+
hooks:
|
|
14
|
+
# Python lint (fast); fails if any issue is found.
|
|
15
|
+
- id: ruff
|
|
16
|
+
args: [--fix]
|
|
17
|
+
# Format Python code.
|
|
18
|
+
- id: ruff-format
|
|
19
|
+
|
|
20
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
21
|
+
rev: v5.0.0
|
|
22
|
+
hooks:
|
|
23
|
+
- id: trailing-whitespace
|
|
24
|
+
- id: end-of-file-fixer
|
|
25
|
+
- id: check-yaml
|
|
26
|
+
- id: check-toml
|
|
27
|
+
- id: check-added-large-files
|
|
28
|
+
- id: debug-statements
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file guides Claude Code (and Claude agents) when working in this repo.
|
|
4
|
+
|
|
5
|
+
## The project in one sentence
|
|
6
|
+
|
|
7
|
+
Python package + Robot Framework library that compares a website's rendering
|
|
8
|
+
between **two Firefox versions** driven in parallel via Selenium.
|
|
9
|
+
|
|
10
|
+
## Structure
|
|
11
|
+
|
|
12
|
+
- `selenium_screenshot_compare/` — installable package (flat layout).
|
|
13
|
+
- Plain Python core: `capture.py`, `comparison.py`, `session.py`,
|
|
14
|
+
`storage.py`, `naming.py`.
|
|
15
|
+
- `ScreenshotCompareLibrary.py`: Robot Framework wrapper around the core.
|
|
16
|
+
**Do not** duplicate business logic here — only `@keyword` methods.
|
|
17
|
+
- `tests/` — example `.robot` suites; they import `Library
|
|
18
|
+
selenium_screenshot_compare.ScreenshotCompareLibrary` (the package must be
|
|
19
|
+
installed, see README).
|
|
20
|
+
- `resources/env/` — storage state (`auth.json`), **gitignored**. Contains
|
|
21
|
+
session cookies: never commit, never log the contents.
|
|
22
|
+
- `firefoxes/` — downloaded Firefox binaries, **gitignored**.
|
|
23
|
+
- `output/` — captures, diffs, `result.json`, Robot reports, **gitignored**.
|
|
24
|
+
|
|
25
|
+
## Installation convention
|
|
26
|
+
|
|
27
|
+
Always editable with the `dev` extra during development:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
./.venv/bin/pip install -e ".[dev]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The `requirements*.txt` files just forward to `pip install -e .[…]` — they
|
|
34
|
+
exist for backwards compatibility, but `pyproject.toml` is the source of truth.
|
|
35
|
+
|
|
36
|
+
## Code style
|
|
37
|
+
|
|
38
|
+
- Python **3.10+** (we use `X | None`, `from __future__ import annotations`
|
|
39
|
+
is already in place).
|
|
40
|
+
- Ruff is configured in `pyproject.toml` (`[tool.ruff]`, `line-length = 100`,
|
|
41
|
+
rule set `E,F,W,I,UP,B,SIM,RUF`).
|
|
42
|
+
- Docstrings and comments **in English**.
|
|
43
|
+
- No abstractions/factories/speculative layers. The code is intentionally flat.
|
|
44
|
+
|
|
45
|
+
## Quality — hooks
|
|
46
|
+
|
|
47
|
+
The project uses `pre-commit` with:
|
|
48
|
+
|
|
49
|
+
- **ruff** (Python lint + format)
|
|
50
|
+
- **robocop / robocop-format** (`.robot` lint + format)
|
|
51
|
+
- standard **pre-commit-hooks** (whitespace, EOF, YAML/TOML, large files,
|
|
52
|
+
`debug-statements`)
|
|
53
|
+
|
|
54
|
+
To do once per clone:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
./.venv/bin/pre-commit install
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
After any substantive change, run:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
./.venv/bin/pre-commit run --all-files
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Never commit with `--no-verify` unless explicitly authorised — if a hook
|
|
67
|
+
fails, fix the root cause.
|
|
68
|
+
|
|
69
|
+
## Continuous integration
|
|
70
|
+
|
|
71
|
+
`.github/workflows/ci.yml` runs on every push and pull request:
|
|
72
|
+
|
|
73
|
+
- `pre-commit run --all-files` on Python 3.12
|
|
74
|
+
- Package build (`python -m build`)
|
|
75
|
+
- `robot --dryrun tests/` to verify the library imports cleanly
|
|
76
|
+
|
|
77
|
+
If you change tooling (Ruff rules, Robocop config, hook versions), update the
|
|
78
|
+
workflow accordingly.
|
|
79
|
+
|
|
80
|
+
## Versioning
|
|
81
|
+
|
|
82
|
+
The version is derived from **git tags** by `setuptools-scm` — there is no
|
|
83
|
+
hardcoded version string to maintain. `__version__` reads it from
|
|
84
|
+
`importlib.metadata` at runtime. To release: tag `vX.Y.Z` on `main` and push;
|
|
85
|
+
the release workflow handles the rest (build + PyPI + GitHub Release).
|
|
86
|
+
|
|
87
|
+
## Adding a Robot Framework keyword
|
|
88
|
+
|
|
89
|
+
1. Write the logic in a Python module of the package (not in
|
|
90
|
+
`ScreenshotCompareLibrary.py`).
|
|
91
|
+
2. Expose a clean function/object in `selenium_screenshot_compare/__init__.py`
|
|
92
|
+
if it belongs to the public API.
|
|
93
|
+
3. Add a `@keyword("Human Readable Name")` method in
|
|
94
|
+
`ScreenshotCompareLibrary.py` that delegates to the core.
|
|
95
|
+
4. Document the keyword (short docstring) and add it to the README table.
|
|
96
|
+
|
|
97
|
+
## Robot Framework — good to know
|
|
98
|
+
|
|
99
|
+
- The `.robot` files load the library by **module name**:
|
|
100
|
+
`Library selenium_screenshot_compare.ScreenshotCompareLibrary`. This
|
|
101
|
+
requires the package to be **installed** in the active venv.
|
|
102
|
+
- Windows paths inside `.robot` files must use forward slashes `/` (`\` is a
|
|
103
|
+
Robot Framework escape character).
|
|
104
|
+
- The comparison is **pixel-by-pixel**; a vertical offset inflates the
|
|
105
|
+
percentage even when the pages look identical. `first_diff_y` helps tell
|
|
106
|
+
the two cases apart.
|
|
107
|
+
|
|
108
|
+
## What not to do
|
|
109
|
+
|
|
110
|
+
- Do not reintroduce the old `sys.path.insert` hack in the Robot Framework
|
|
111
|
+
library — the package is now importable normally.
|
|
112
|
+
- Do not read or commit `resources/env/auth.json` (secrets).
|
|
113
|
+
- Do not install geckodriver manually: Selenium Manager handles it.
|
|
114
|
+
- Do not add a runtime dependency without updating `pyproject.toml`
|
|
115
|
+
(`dependencies` for the core, `optional-dependencies` for the rest).
|
|
116
|
+
|
|
117
|
+
## Checks before saying "it's ready"
|
|
118
|
+
|
|
119
|
+
- `./.venv/bin/pre-commit run --all-files` passes.
|
|
120
|
+
- `./.venv/bin/python -c "import selenium_screenshot_compare as s;
|
|
121
|
+
print(s.__version__)"` responds without error.
|
|
122
|
+
- If a `.robot` moved: `./.venv/bin/robot --dryrun tests/` stays green
|
|
123
|
+
(requires Firefox but at least validates the library import).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Development setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
python3 -m venv .venv
|
|
7
|
+
./.venv/bin/pip install -e ".[dev]"
|
|
8
|
+
./.venv/bin/pre-commit install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Linting & formatting
|
|
12
|
+
|
|
13
|
+
Orchestrated by **pre-commit** — every `git commit` runs them automatically.
|
|
14
|
+
|
|
15
|
+
| Tool | Scope | What it does |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| **Ruff** | Python (`selenium_screenshot_compare/`) | lint + format (replaces flake8 / black / isort) |
|
|
18
|
+
| **Robocop** | `.robot` files | lint + format |
|
|
19
|
+
| **pre-commit-hooks** | everything | trailing whitespace, EOF, YAML/TOML, large files, debug statements |
|
|
20
|
+
|
|
21
|
+
Run manually:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
./.venv/bin/ruff check selenium_screenshot_compare/ # Python lint
|
|
25
|
+
./.venv/bin/ruff format selenium_screenshot_compare/ # Python format
|
|
26
|
+
./.venv/bin/robocop format # format .robot
|
|
27
|
+
./.venv/bin/robocop check # lint .robot
|
|
28
|
+
./.venv/bin/pre-commit run --all-files # everything at once
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Continuous integration
|
|
32
|
+
|
|
33
|
+
A [GitHub Actions workflow](.github/workflows/ci.yml) runs on every push and
|
|
34
|
+
pull request:
|
|
35
|
+
|
|
36
|
+
- **Lint** — `pre-commit run --all-files` (Python 3.12)
|
|
37
|
+
- **Smoke test** — install + import on Python 3.10 and 3.14
|
|
38
|
+
- **Build** — `python -m build`, install the wheel, verify the import
|
|
39
|
+
- **Robot dry-run** — `robot --dryrun tests/` to validate the library loads
|
|
40
|
+
|
|
41
|
+
## Checks before submitting
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
./.venv/bin/pre-commit run --all-files
|
|
45
|
+
./.venv/bin/python -c "import selenium_screenshot_compare as s; print(s.__version__)"
|
|
46
|
+
./.venv/bin/robot --dryrun tests/
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Releasing
|
|
50
|
+
|
|
51
|
+
The version is derived automatically from git tags by **setuptools-scm** — no
|
|
52
|
+
version string to maintain manually.
|
|
53
|
+
|
|
54
|
+
To publish a new release:
|
|
55
|
+
|
|
56
|
+
1. Merge `dev` into `main`.
|
|
57
|
+
2. Tag and push: `git tag v0.1.0 && git push origin v0.1.0`
|
|
58
|
+
3. The [release workflow](.github/workflows/release.yml) automatically:
|
|
59
|
+
- Builds the sdist + wheel
|
|
60
|
+
- Publishes to **PyPI**
|
|
61
|
+
- Creates a **GitHub Release** with auto-generated release notes
|
|
62
|
+
|
|
63
|
+
> First-time setup: configure [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/)
|
|
64
|
+
> for the repository — add a publisher with workflow `release.yml` and
|
|
65
|
+
> environment `pypi`. No API token needed after that.
|