Release Contract¶
.version is the only human-maintained stable semantic version. After a
release, leave it at the published value while ordinary changes accumulate in
the ## Unreleased section of CHANGELOG.md. A pull request does not imply a
new release, and any number of merged pull requests may share that unreleased
section without changing package metadata or invoking publication.
When a maintainer deliberately prepares a release, advance .version once,
move the accumulated notes into one dated ## [X.Y.Z] - YYYY-MM-DD section,
retain the empty ## Unreleased heading for later work, and run
make version-check. Dynamic package metadata, elsewindow.__version__, the
vX.Y.Z tag, release name, and generated notes all resolve from that value.
When a pull request changes CHANGELOG.md, repository automation copies its
newest populated level-two section into one managed block in the pull-request
body. This means populated Unreleased notes are shown even though .version
is unchanged. Manual text outside the block remains contributor-owned;
malformed or duplicate markers fail closed.
The release inventory is exact:
| Destination | Artifacts |
|---|---|
| PyPI | wheel and sdist only |
| GitHub Release | the same wheel and sdist bytes, both Linux executables, SHA256SUMS.txt |
Reusable CI builds every byte once. Publication jobs only download those verified artifacts; they never rebuild. Equivalent clean source trees must produce byte-identical normalized wheel and sdist bytes. Each native PyInstaller build instead carries checked provenance because the tool does not promise a portable byte-reproducible one-file executable.
On every direct main push, the state job first inspects PyPI and GitHub
independently. Release need is determined by that external state, never merely
by whether .version changed. Existing project, version, tag, tagged source
commit, notes, filename, type, size, SHA-256, yanked state, executable
inventory, and checksum bytes must all match. A complete published match is a
no-op even when main and Unreleased have advanced: the reusable release CI
and both publication jobs are skipped, and metadata is checked against the
immutable tagged source instead of the new head. An exact draft may resume by
uploading only missing artifacts. Unexpected, duplicate, incomplete
published, moved-tag, or byte-conflicting state fails without deletion or
overwrite.
PyPI publication uses the pinned official action, one GitHub Environment named
pypi, and job-scoped id-token: write. No API token or password is accepted.
GitHub publication starts only after PyPI succeeds or is already an exact
match. It creates or recovers an exact draft, verifies every existing and new
asset, then publishes the immutable vX.Y.Z release.
Before the first push, the operator must:
- run every local gate against the reviewed working tree, then create and review the first commit without treating commit identity as validation evidence;
- create the GitHub
pypiEnvironment; - configure the pending PyPI Trusted Publisher for repository
kogeler/elsewindow, workflowrelease.yml, environmentpypi; - enable GitHub Pages with GitHub Actions as the source and protect the
github-pagesEnvironment according to repository policy; - enable private vulnerability reporting and configure branch/ruleset protections, CODEOWNERS review, conversation resolution, and the stable checks exposed by hosted CI;
- push without amending while publication runs and verify hosted hashes and the canonical documentation site.
Local gates always read the current filesystem bytes, including maintained changes that are not committed. Distribution normalization uses a fixed ZIP-compatible epoch and never reads a commit timestamp. Artifact and source digests describe produced release evidence; they are not cached gate results and cannot authorize skipping a later validation run.
Pages publication is owned only by the narrow workflow described in the CI contract. Never create a tag, release, upload, publisher, or repository setting as part of a local validation run.