Package naming

PLESTY packages follow a fixed naming convention:

Convention Example
Distribution name (PyPI, pyproject.toml [project] name) plesty- plesty-power-meter
Import package (namespace) plesty. plesty.power_meter

The distribution name is what users type in pip install / uv add; the import package lives inside the shared plesty namespace. plesty init scaffolds both correctly — verify they still match before the first release, because the distribution name on PyPI cannot be changed later without publishing under a new name.

One-time setup: how the release job authenticates

The release job (Gate 14) publishes in one of two ways, and picks between them by whether a token is configured.

Trusted publisher (preferred — no stored secret)

Leave PYPI_TOKEN unset and the job authenticates with a PyPI trusted publisher. It exchanges the GitLab OIDC ID token issued to that pipeline, audience pypi, for an upload token valid for minutes. Nothing long-lived is stored in GitLab, and nothing has to be rotated.

Register the publisher once, on PyPI, under the project's Publishing settings: give it the GitLab namespace, the project name, and .gitlab-ci.yml as the pipeline file, exactly as they are on GitLab. PyPI checks the ID token against that registration and refuses anything else.

Two failures point at this setup, and the job names both:

the registration is missing, or its namespace, project or pipeline file does not match.

API token (fallback)

Set PYPI_TOKEN and the job uploads with it instead, which is what you need where a trusted publisher cannot be registered:

  1. Create the token on PyPI: pypi.org → Account settings → API tokensAdd API token. After the first release exists, replace it with a token scoped to the single project instead of the whole account.
  2. Add it as a CI/CD variable: in the GitLab project, Settings → CI/CD → Variables → add PYPI_TOKEN, flagged Masked and Protected.
  3. Protect the release tags: Settings → Repository → Protected tags → protect the pattern v*.

Step 3 is the one that is easy to miss: a protected variable is only exposed to pipelines running on protected branches or tags. If v* tags are not protected, every gate passes but the release job fails with PYPI_TOKEN is empty. If you see that error on an otherwise green pipeline, check the protected-tags setting first.

Protecting v* is still worth doing with a trusted publisher, but for a different reason: no

credential depends on it any more, so it controls who is allowed to cut a release rather than

whether the upload can authenticate.

For a dry run against TestPyPI (see below), neither is needed — the upload runs locally with a TestPyPI token.

Pre-flight checklist

Before tagging, verify everything is in order:

# 1. Run the full quantum check
uv run plesty check --standard quantum

# 2. Review unreleased commits
git log $(git describe --tags --abbrev=0 2>/dev/null || echo "")..HEAD --oneline

# 3. Confirm CHANGELOG is updated
cat CHANGELOG.md | head -20

# 4. Confirm the current branch is clean
git status

All gates must pass. The CHANGELOG must have an entry for this release. Do not tag until the check passes — CI Gate 14 runs the same check in a clean environment and will reject the build if it fails.

Determine the version bump

Commits since last tag contain Bump
Any feat: minor: v0.2.xv0.3.0
Only fix:, chore:, docs:, refactor: patch: v0.2.1v0.2.2
Any feat!: or BREAKING CHANGE:, below 1.0.0 minor: v0.2.xv0.3.0
Any feat!: or BREAKING CHANGE:, from 1.0.0 on major: v1.2.xv2.0.0

A patch may never change the public API, so a breaking change always opens a new line. See Branch Discipline for what a line is and which branch each release is tagged on.

Merge, tag and push

A minor release opens a new line, so it goes through main:

git checkout main
git merge --no-ff exp          # the one merge that moves main; its diff is the changelog
git tag v0.3.0
git push origin main v0.3.0

git checkout exp               # merge back immediately, every time
git merge main
git push origin exp

A patch on a line is merged from release/0.N instead — to main while that line is the newest released one, and to exp once its successor has shipped, in which case the tag goes on the line itself.

Do not push the tag to a branch — push the tag directly. GitLab detects the v* pattern and triggers Gate 14.

What happens in CI after the tag push

  1. check stage: plesty check --standard quantum runs in a clean environment
  2. security stage: secret scan
  3. deploy stage: Sphinx docs build → push to docs-build branch
  4. release stage: uv builduv publish to PyPI, authenticating through the trusted publisher, or with PYPI_TOKEN where one is set

The package is live on PyPI within a few minutes of the pipeline completing. It will be installable as:

pip install plesty-power-meter==0.3.0
# or
uv add plesty-power-meter==0.3.0

Monitoring the pipeline

glab ci list -R plesty/hub/devices/<vendor>/<project> --ref v0.3.0

Or watch on the GitLab web UI under CI/CD → Pipelines.

Dry run: the TestPyPI channel

Before a first real release, verify the packaging end-to-end on TestPyPI. Build locally and upload with a TestPyPI token:

uv build
uv run twine upload --repository testpypi dist/*

Then install from the test channel in a clean environment (--extra-index-url lets dependencies resolve from real PyPI):

pip install --index-url https://test.pypi.org/simple/ \
            --extra-index-url https://pypi.org/simple/ \
            <your-package>

Runnable end-to-end example

The demo experiment (plesty-demo-experiment) is published on the TestPyPI channel and runs a mock line scan with no hardware attached — it is the reference for what a finished, published module looks like:

pip install --index-url https://test.pypi.org/simple/ \
            --extra-index-url https://pypi.org/simple/ \
            plesty-demo-experiment
python -m plesty.demo_experiment --points 5

After the release

  1. Update docs/index.md to reference the new version (if it mentions a version number)
  2. Start the next CHANGELOG section:
## Unreleased

(no changes yet)
  1. Notify the team if this release has API changes that downstream experiments must accommodate