feature:release
Table of contents
- Introduction
- How a release runs
- Choosing the version
- Pinning the new version in your product
- Rehearsing a release
- Where the tests run
- Publishing to one channel
- Releasing under a PyPI name of your own
- Shipping the frontend bundle
- Fixing what stops a release
- Advanced
- Reference
- Requirements
- See also
Introduction
feature:release publishes a new version of a feature you develop at the workspace root. It checks the feature, bumps the version, uploads it to PyPI, tags and releases it on the forge, and keeps a read-only copy of the tag in the cache. Run it when your changes are ready for products to pin.
splent feature:release splent_feature_notes
Reading the current version...
Current version: v1.2.6
Bump type:
[1] patch v1.2.7 bug fixes, no new features
[2] minor v1.3.0 new features, backward compatible
[3] major v2.0.0 breaking changes
[4] cancel
Choice: 1
Will release as v1.2.7
Proceed? [y/N]: y
Releasing splent_io/splent_feature_notes v1.2.7
Release gate (channels)
forge ready, splent-io/splent_feature_notes accepts push, tag and release as alice
pypi ready, credentials accepted, splent_feature_notes exists so this is a new version
bundle building assets/dist in production mode...
bundle built, 2 file(s) in assets/dist
Pre-release checks (lint + tests)
checks ruff (lint + format)...
checks lint OK
checks tests...
checks tests OK
Re-checking the channels, right before publishing
forge ready, splent-io/splent_feature_notes accepts push, tag and release as alice
pypi ready, credentials accepted, splent_feature_notes exists so this is a new version
version 1.2.7 written to pyproject.toml
contract updating from source code...
contract written to pyproject.toml
bundle assets/dist goes into the release commit
commit changes committed locally
pypi building package...
pypi uploading...
pypi upload complete
push origin/main updated
tag v1.2.7 created
tag v1.2.7 pushed to origin
forge release created https://git.splent.io/splent-io/splent_feature_notes/releases/tag/v1.2.7
snapshot cloning splent_feature_notes@v1.2.7...
snapshot /workspace/.splent_cache/features/splent_io/splent_feature_notes@v1.2.7 (read-only)
index marketplace index rebuild requested, so this version appears there within a couple of minutes.
splent_io/splent_feature_notes v1.2.7 released.
Attach splent_feature_notes@v1.2.7 to my_first_app? [Y/n]:
The pipeline and its gates are the same for every SPLENT release. Release explains them.
How a release runs
Nothing changes until the second gate passes, so a failing check leaves no version bumped. The release also refuses, before it starts, a contract requirement that is not PEP 440, a migration id too long, an SQL or XSS sink and a breaking parameter change the version does not announce.
Choosing the version
splent feature:release splent_feature_notes v1.2.7
splent feature:release splent_feature_notes --bump patch
Name the version, or pass --bump, to skip the wizard. The version is MAJOR.MINOR.PATCH, with or without the v.
Both the wizard and --bump start from the highest version among the forge’s tags, the local tags and [project].version, or from v0.0.0. When the forge cannot list its tags and nothing local knows a version, the command refuses to guess.
Pinning the new version in your product
splent feature:release splent_feature_notes --attach
At the end, the release offers to pin the new version in the active product with feature:attach. --attach pins it without asking. Without a terminal, as in CI, it prints the feature:attach command instead.
Rehearsing a release
splent feature:release splent_feature_notes --dry-run --bump major
--dry-run runs every check a real release runs, builds the package in a temporary copy, and changes nothing. It needs no credentials. It prints what a real run would do, then the verdict.
Dry run. Nothing was changed: no file, no commit, no tag, nothing pushed or uploaded.
A real release would be refused right now:
- forge: SPLENT_FORGE_TOKEN is not set
- pypi: PyPI credentials are not set
It exits 0 when a real release would go through, and 1 otherwise. Without a terminal, pass the version or --bump, since the wizard cannot ask.
Where the tests run
splent feature:release splent_feature_notes --neutral
The gate runs the tests of the working copy you release. Where they run depends on the active product.
| The active product | Without --neutral |
With --neutral |
|---|---|---|
| declares the feature | In the product’s app. A failure stops the release | In a neutral app, which gates the release. Then in the product’s app, as a check that only warns |
| does not declare it | In a neutral app, which gates the release | The same |
The neutral app holds the framework, the feature, the newest releases of what it requires and the default configuration. Nothing of your product reaches it but its test database. Use --neutral when a template or setting of your product should not decide the release of a feature other products use.
Publishing to one channel
splent feature:release splent_feature_notes --no-pypi
splent feature:release splent_feature_notes --no-forge
--no-pypi publishes to the forge only. A production image then installs the version from the tag on the forge.
--no-forge publishes to PyPI only. The commit and the tag stay on this machine, and there is no snapshot and no attach step. Once release:resume has pushed the tag, run the feature:attach command the release prints.
Both ask you to confirm. A feature that never publishes to a channel declares it once, under [tool.splent.release] channels, and is not asked again.
Releasing under a PyPI name of your own
[project]
name = "splent-feature-notes-alice"
The PyPI project is [project].name. When a feature’s name is taken on PyPI, give your copy another one. The import package and the name products use stay the same.
feature:attach records the new name in the product’s [tool.splent.distributions], where feature:pip-install and product:build read it. The forge repository is whatever origin points at, so it can live under your own account.
Shipping the frontend bundle
Every channel installs what the tag holds, so the tag must carry the bundle. The release builds it before the tests, in production mode, into an emptied assets/dist, and commits it with the code.
| The feature has | The bundle is built |
|---|---|
assets/js/webpack.config.js |
With feature:compile --release, in the active product’s web container |
frontend/package.json |
In the SPLENT toolchain image, with its licences and audit gated, see A feature with frontend/ |
| Neither | Not at all |
A bundle that fails to build stops the release, with nothing changed.
error: the frontend bundle of splent_io/splent_feature_notes was not built: feature:compile --release failed.
ERROR in ./scripts.js
Module not found: Error: Can't resolve 'mustache'
The release tags the bundle with the code, so it does not go on without one.
webpack runs in the product's web container (splent product:up --dev starts it).
Try the build on its own with: splent feature:compile splent_io/splent_feature_notes --release
No version bump, no commit, no tag, no release.
A feature with frontend/ also commits assets/meta, with the manifest, third_party.json and an SBOM. The manifest is checked against the files once more before the commit, as feature:compile --verify does.
Stop any
feature:compile --watchof the same copy before you release. Ifassets/distchanges after the build, the release stops with the version bumped but not committed.
Licences and audit
What a bundle may ship depends on the feature’s [project].license. The gate reads the licence of every package the bundle ships, from assets/meta/third_party.json, and of every wheel in splent.vendored.
| A package under | LGPL feature, or any other licence | GPL feature | AGPL feature |
|---|---|---|---|
| MIT, ISC, BSD-2/3-Clause, 0BSD, Apache-2.0, Zlib, CC0-1.0, Unlicense, BlueOak-1.0.0, Python-2.0 | ships | ships | ships |
| MPL-2.0, LGPL-2.1, LGPL-3.0 | ships | ships | ships |
| GPL-2.0-or-later, GPL-3.0 | stops | ships | ships |
| GPL-2.0-only | stops | stops | stops |
| AGPL-3.0 | stops | warns | ships |
| OFL-1.1, CC-BY-4.0, LPPL-1.3c | warns | warns | warns |
No licence, UNLICENSED, SEE LICENSE IN …, an unknown one |
stops | stops | stops |
An SPDX OR takes the best of its options, and an AND the worst. A package published without its licence file takes the text the feature names for it in splent.licenseTexts.
To ship a package that would stop the release, add an exception to frontend/package.json.
"splent": {
"licenseExceptions": [
{ "package": "some-editor", "reason": "replaced by Markdown in the next major", "expires": "2027-03-31" }
]
}
The release warns 30 days before the expiry, and after it the exception lets nothing through. npm audit stops the release on a critical vulnerability, or when it cannot reach the registry, and warns on a high one.
Fixing what stops a release
A breaking parameter change
error: since v1.4.0 the parameters of splent_feature_notes changed in a way that breaks a product that sets them:
NOTES_VIEW no longer accepts mosaic
v1.5.0 keeps the major version of v1.4.0. Release it with --bump major, or keep the old declaration compatible.
The release compares the feature’s [tool.splent.config_schema] with the one at its latest vX.Y.Z tag. A change that breaks a product setting a parameter needs a new major version, or a new minor one below 1.0.0.
| Breaks a product | Does not |
|---|---|
| A key removed, or its type or items changed | A key added |
| A choice dropped, or choices added to a key that had none | A choice added |
min raised, max lowered, a pattern added or changed |
A range widened |
A key made required |
A default, label or help changed |
A migration revision id too long
error: these revision ids are longer than the 32 characters alembic_version.version_num holds, so the release did not start:
notes0011_entries_handed_over_all.py: 'notes0011_entries_handed_over_all' is 33 characters
No database could record that migration after running it. Shorten the revision id and every down_revision that names it. feature:contract refuses it too.
An SQL or XSS sink
error: splent_feature_notes has 1 SQL or XSS sink(s) the toolchain refuses, so the release did not start.
routes.py:42 [sql-from-string] text() takes SQL built with an f-string
Bind the values, text("... WHERE id = :id").bindparams(id=value), or use the ORM.
The release reads the feature’s code and templates as feature:security does, and --skip-checks does not skip it. Fix each line, or mark a line you have reviewed with # splent: allow <rule>.
A previous release that never finished
The release refuses to start while the previous version is on one channel and missing from the other. Finish it with release:resume. If it will never be finished, name the new version and pass --allow-unfinished.
The PyPI new-project limit
splent feature:release splent_feature_tags v0.1.0 --wait
A feature’s first release creates its PyPI project, and PyPI limits how many projects you create. SPLENT counts them in <WORKING_DIR>/.splent/pypi_creations.json and stops two short of each limit. A wait up to 65 minutes happens with a countdown. A longer one stops the release before anything changes and says when to come back, unless you pass --wait.
A new version of an existing project never waits. PyPI new-project limits has the limits and the variables that tune them.
A failure after the upload
error: splent_io/splent_feature_notes v1.2.7 is already on PyPI, and a later step failed.
The channels have DIVERGED for this version. Finish it, without bumping the version, with
splent release:resume splent_feature_notes
A version on PyPI cannot be taken back. Fix the cause and run the command it prints, which finishes the same version.
Advanced
Feature reference
The feature is name or namespace/name, and the namespace defaults to splent_io. Each part holds letters, digits, _, - and ., so a path such as ../x is refused, and so is @version. release:resume applies the same rule.
What the release writes
- The contract.
[tool.splent.contract]is rewritten from the source, asfeature:contract --writedoes. A feature withfrontend/also gets[tool.splent.contract.frontend], a projection of the bundle’s manifest. Never edit it by hand. - The commit. Every change of the working tree, except files that were untracked before the release started.
- The push. To the branch
origincalls its default. Pushes and fetches never ask for a password and give up after 10 minutes.
The cache snapshot
The snapshot is a read-only clone of the tag at .splent_cache/features/<namespace>/<feature>@<version>/. An existing clone of that tag is reused. Anything else at that path stops the run, so move it aside and run release:resume.
--skip-checksskips lint and tests, for emergencies only. The channel gate, the security check and the bundle still run.
Reference
Arguments and options
| Argument / Option | Description |
|---|---|
FEATURE_REF |
splent_feature_notes or splent-io/splent_feature_notes |
VERSION |
The version to release, such as v1.2.7. Without it, the wizard asks |
--bump [patch\|minor\|major] |
Bump the current version without asking. Not with VERSION |
--dry-run |
Rehearse the release and change nothing |
--attach |
Pin the released version in the active product without asking |
--skip-checks |
Skip lint and tests |
--no-pypi |
Publish to the forge only |
--no-forge |
Publish to PyPI only |
--allow-unfinished |
Release although the previous version never finished |
--wait |
Wait for the PyPI new-project limit however long it takes |
--neutral |
Gate on the tests in a neutral app |
Exit codes
| Code | When |
|---|---|
| 0 | Released, or a dry run that would go through |
| 1 | Refused or failed, or a dry run that would be refused |
| 2 | A usage error, such as VERSION together with --bump |
Errors
| Message | What to do |
|---|---|
No product selected |
Select one with product:select, or use --dry-run |
editable feature not found at |
Put the feature at the workspace root, or feature:unlock it |
is not a valid feature reference |
See Feature reference |
is not a release version |
Use MAJOR.MINOR.PATCH |
these requirements of the contract are not valid |
Write a PEP 440 specifier, see Version floors |
these revision ids are longer than the 32 characters |
See A migration revision id too long |
SQL or XSS sink(s) the toolchain refuses |
See An SQL or XSS sink |
the parameters of <feature> changed |
See A breaking parameter change |
That release never finished |
See A previous release that never finished |
lint failed |
Run splent lint --fix |
tests failed for <feature> |
Fix them, see Where the tests run |
the frontend bundle of <feature> was not built |
See Shipping the frontend bundle |
no version under [project] |
Add version = "0.1.0" under [project] |
<tag> already exists and points at another commit |
Release a new version |
already exists and is not a clone of <tag> |
Move the path aside, then release:resume |
The channels have DIVERGED |
Run the release:resume command it prints |
Requirements
- The feature at the workspace root, an editable copy with an
originremote. Runfeature:unlockon a pinned one first. - A selected product (
SPLENT_APP), except for--dry-run. Its web container runs the tests and a webpack bundle, so start it withsplent product:up --dev. - PyPI credentials,
TWINE_USERNAMEandTWINE_PASSWORD, orPYPI_USERNAMEandPYPI_PASSWORD. SPLENT_FORGE_TOKEN, to push, tag and create the release page.GITHUB_TOKENis read when it is not set.
See also
feature:contractinfers or updates the contract on its own.feature:attachpins a released version in a product.release:resumefinishes a release that stopped halfway.- Release explains the pipeline and the gates every release shares.
- Feature contract describes the contract format.