How a dikw-converter-* plugin gets from a workspace package to a
versioned PyPI release. This is the operational manual for maintainers;
plugin authors should read plugin-author-guide.md
§ 6 for the author-side checklist that precedes this.
dikw-converter-<format>-v<X.Y.Z>
<format>matches the package directory name underpackages/(e.g.dikw-converter-epub).<X.Y.Z>is the SemVer 2.0 version exactly as it appears in that package'spyproject.toml. Pre-release suffixes (-rc.1,.dev0, …) are accepted by the workflow's regex.- Push the tag — that alone triggers
.github/workflows/release.yml. Nothing else publishes; the tag is the sole release switch.
.github/workflows/release.yml, in order:
- Parse the tag into
packageandversionusing the regex^(dikw-converter-[a-z0-9-]+)-v([0-9]+\.[0-9]+\.[0-9]+[a-zA-Z0-9.+-]*)$. - Checkout this repo.
dikw-coreis pulled from PyPI by each plugin's own[project.dependencies]; no sibling checkout needed. - Verify the tag version equals
packages/<package>/pyproject.toml'sproject.version— a tag without a matching version bump fails fast. - Sync the uv workspace with
uv sync --all-extras. - Extract the CHANGELOG block via
scripts/extract_changelog.py packages/<package>/CHANGELOG.md <version>into${RUNNER_TEMP}/release-notes.md. Missing block ⇒ fail. - Run the per-package unit tests with
uv run pytest packages/<package> -v. These are the plugin's own tests — no-kfilter so every test in the package'stests/dir runs. - Run the artifact-level packaging gate with
uv run pytest tests/packaging -k <package-underscore-form> -v, where the underscore form is<package>with-replaced by_(matches the parametrize ids set intests/packaging/conftest.py). Steps 6 and 7 are separate so the artifact gate's-kfilter cannot accidentally deselect the package's own unit tests. - Build with
uv build --package <package> --out-dir dist. twine check --strictondist/*.whl dist/*.tar.gz(defense-in-depth; the artifact tests already did this, but build environments can drift).- Publish to PyPI via
pypa/gh-action-pypi-publishusing OIDC trusted publishing — no API token in repo secrets. - Create a GitHub Release whose body is the extracted CHANGELOG block, with the wheel and sdist attached.
This is a one-time PyPI-side configuration per package name. Two flavours:
- First-ever release of a new package name (the name has never existed on PyPI): use the Pending Publisher form. PyPI reserves the name to be claimed on first publish from a matching workflow.
- Subsequent releases / pre-existing names: under
https://pypi.org/manage/project/<package>/settings/publishing/, add a GitHub Trusted Publisher with:- Owner:
opendikw - Repository:
dikw-plugins - Workflow filename:
release.yml - Environment: (leave blank)
- Owner:
No API tokens are stored in GitHub secrets — OIDC issues a short-lived token at publish time scoped to that one workflow run.
Before tagging, always run:
uv run python scripts/check-package.py dikw-converter-<format>This composes the same checks release.yml runs:
scripts/extract_changelog.py— fails if no CHANGELOG entry for the currentpyproject.tomlversion, and prints the would-be release notes for human review.uv run pytest tests/packaging -k <package> -v— the six artifact tests per package. Build,twine check --strict, entry-point registration, METADATA consistency, install-into-clean-venv discovery, and CHANGELOG presence are all driven by the fixtures — no separateuv buildinvocation is needed.
If the local gate fails, the CI gate will fail the same way — fix and
re-run before tagging. To inspect the actual wheel/sdist files
(filenames, sizes), run uv build --package <package> directly.
If a release was accidentally published or a critical bug surfaces post-publish:
-
Yank on PyPI (does not delete; signals "don't auto-install"):
pip install --upgrade twine twine yank dikw-converter-<format>==X.Y.Z --reason "<why>"
Or use the project settings page under
https://pypi.org/manage/project/<package>/release/<version>/. PyPI does not allow deleting a released version — yanking is the maximal correction. -
Delete the GitHub Release (the tag stays, but the published artifacts and notes go away):
gh release delete dikw-converter-<format>-vX.Y.Z
-
Cut a fix release at
X.Y.(Z+1). Do not retagX.Y.Z— PyPI'sskip-existing: truewould silently skip the re-publish and you'd ship nothing.
For a regression in a released version:
-
Branch off the release tag if
mainhas moved on:git checkout -b hotfix/<format>-X.Y.Z+1 dikw-converter-<format>-vX.Y.Z
-
Bump version to
X.Y.(Z+1), add a CHANGELOG entry under### Fixed, commit. -
Run
scripts/check-package.py. -
Merge the hotfix back to
main, then tag frommain(so the tag commit is part of the linear history).
.github/workflows/release.yml— the pipeline driving everything.scripts/extract_changelog.py— used byrelease.ymlandcheck-package.py.scripts/check-package.py— local pre-release orchestrator.tests/packaging/— artifact-level gate.packages/<package>/pyproject.toml— the source of truth forname,version,dependencies, and the entry-points table.packages/<package>/CHANGELOG.md— the source of truth for release notes.