Release

Commands for releasing SPLENT packages to GitHub and PyPI.


Table of contents

  1. The commands
  2. The pipeline
  3. The channel gate
  4. Declaring channels
  5. Pre-release gate (lint + tests)
  6. Afterwards

The commands

Command Releases
release:cli splent_cli
release:framework splent_framework
feature:release One feature
product:release One product, Docker image included
release:resume Nothing new. Finishes an interrupted release

The first four share the same pipeline from services/release.py. Each adds its own specifics, contract inference for features, Docker for products.

When invoked without an explicit version they run an interactive semver wizard that reads the current version from GitHub and offers patch, minor, major, or cancel.

Publishing a feature needs no account and no token beyond your own GitHub and PyPI credentials. There is nothing to log in to, and the tag is the publication. splent login is only for spl:publish, which is a different thing.


The pipeline

The order below is the pipeline, and the position of the PyPI upload is deliberate.

  1. Validate the environment, meaning the credentials the enabled channels need are present.
  2. Channel gate, pass one. Cheap and early, so a bad token or an already published version costs seconds instead of a full test run.
  3. Lint and tests.
  4. Channel gate, pass two. The binding one, so the verdict acted on is seconds old rather than minutes.

Everything below this line changes something.

  1. Update the version in pyproject.toml, local and revertible.
  2. Optional pre-commit hook, for instance regenerating the contract.
  3. Commit, still local.
  4. Build and upload to PyPI.
  5. Push the branch, push the tag, create the GitHub release.
  6. Optional post hook, the feature snapshot or the Docker image.

A PyPI version can never be replaced or removed. A tag that was never pushed costs nothing. Uploading first means a PyPI refusal, including the rate limit on creating a new project, leaves nothing published anywhere and no divergence to repair. The reverse order is what put thirteen features on GitHub with nothing on PyPI.

If a step after the upload fails, the run exits non zero, calls the result a divergence in those words, and prints the release:resume command that finishes the job without bumping the version.


The channel gate

A release refuses to start unless every channel it publishes to would accept it right now. Before the first mutation, for this exact repository, package version and image, the gate answers one question.

Will GitHub accept a push, a tag and a release, will PyPI accept this upload, and will Docker Hub accept this image?

Every answer is one of three things and never anything else.

Verdict Meaning
ready Verified, go ahead
blocked A refusal, with the channel named and the cause separated into credentials versus rate limit
skipped You deliberately took this channel out of the release, and the pipeline publishes nothing there

Doubt counts as blocked. A false green costs a permanent divergence, a false red costs waiting. A 429 is never reported as a credential problem and a 401 is never reported as a rate limit, because waiting will not fix a token and a new token will not fix a window.

What the gate refuses on.

  • No GITHUB_TOKEN, a token that cannot push to the repository, or an archived repository.
  • A version lower than the newest tag already on the repo. A tag cannot be taken back, so a version that goes backwards is permanent.
  • A tag that already exists. That version was released before, so the answer is release:resume, not a new number.
  • PyPI credentials missing, rejected, or scoped to another project.
  • A version already on PyPI, which PyPI never accepts twice.
  • A previous release that never finished. Bumping past it burns a version number and leaves the old one broken, so it is a refusal unless you pass --allow-unfinished.

The PyPI probe names the project and version, which is what makes the two failures that actually happen visible in advance, the rate limit on creating a new project (keyed on the name, and far stricter than the limit on a new version of an existing project) and a project scoped token that is not scoped to this project. It posts an upload with no file, so it publishes nothing.

Nothing is mutated when the gate refuses. No version bump, no commit, no tag, no release, and the command says so.


Declaring channels

A package that never goes to PyPI, a private feature for instance, declares it once in its own pyproject.toml.

[tool.splent.release]
channels = ["github"]

Silence means the default, GitHub and PyPI. Anything present but unusable, a misspelled key or an unknown channel name, is an error rather than a fallback. Publishing to more channels than the author asked for is the unrecoverable direction.

Every release command also takes --no-github and --no-pypi for a single run. Those are a different thing from a declaration, since they create an asymmetry nobody can see from the repository, so they are confirmed out loud before anything happens and refused when there is nobody to ask.


Pre-release gate (lint + tests)

Before any irreversible step, every release command lints and tests the entity being released and aborts if either fails.

  • Lint. ruff check + ruff format --check on the entity directory (the same as splent lint).
  • Tests. The entity’s own test suite.

    Command Tests run
    release:cli / release:framework pytest in the package directory
    feature:release feature:test
    product:release product:test

A failure aborts the release with no changes made. Nothing is committed, tagged, or published. Fix the issues (try splent lint --fix for formatting) and re-run.

For emergencies only, every release command accepts --skip-checks to bypass lint and tests. It does not bypass the channel gate.


Afterwards

check:releases compares git tags against PyPI releases for the whole workspace and names anything that diverged, or that it could not check.


Table of contents


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es