Release and deploy

Rehearse the release of splent_feature_notes without publishing anything, then deploy your product to production and create its first user.


Table of contents

  1. Prerequisites
  2. Step 1. Write the feature contract
  3. Step 2. Rehearse the release
  4. Step 3. Build the production image
  5. Step 4. Deploy
  6. Step 5. Create the first user
  7. Step 6. Shut down production
  8. Step 7. Back to development
  9. What you learned
  10. Next

Prerequisites

You have completed Tutorial 4, Testing. You are inside the CLI container with my_first_app selected and splent_feature_notes working with passing tests.

(my_first_app) /workspace$

Part 1. Rehearse a release

A release publishes a feature on two channels. The forge (git.splent.io) gets the commit, the tag and a release page, and PyPI gets the package. A PyPI version is permanent, so you rehearse first.

feature:release --dry-run runs every step before the first irreversible one. It works out the version, checks the channels, runs lint and the tests, writes the contract and builds the package, then says what a real release would do and what would stop it today. It writes, commits and publishes nothing, so you can run it without any account.


Step 1. Write the feature contract

The contract is the machine-readable summary of what your feature provides and requires, in pyproject.toml. The CLI, the release and the marketplace read it.

splent feature:contract splent_feature_notes --write

It prints the inferred contract, as in Tutorial 3, Step 12. You accepted the update feature:xray offered there, so it ends with this line.

  ✅ Contract is already up to date.

Had the source changed since, it would write the new contract to pyproject.toml.

This step is optional, since a release updates the contract itself. Running it first lets you review it.


Step 2. Rehearse the release

splent feature:release splent-io/splent_feature_notes --dry-run --bump major

--bump major answers the version wizard, which otherwise offers patch, minor and major. The current version is the highest of the forge’s tags, the local tags and the version in pyproject.toml (0.0.1 from the scaffold). The forge has no repository for your notes yet, and the wizard says so.

  DRY RUN  nothing will be written, committed, tagged, pushed or uploaded.

  Reading the current version...
  warn: the git host would not list the tags of splent-io/splent_feature_notes (splent-io/splent_feature_notes does not exist or this token cannot see it).
  Using the highest version this clone knows about, 0.0.1. It may be behind the remote.
  Current version: 0.0.1
  major bump: v1.0.0

  Rehearsing the release of splent-io/splent_feature_notes v1.0.0

Then it goes through the gates of a real release, in the same order.

  Git
  git      no 'origin' remote
           The release pushes main and the tag to origin. Add it with: git remote add origin ssh://git@git.splent.io:2223/splent-io/splent_feature_notes.git
  git      <n> uncommitted change(s), the release commit would include them all

  Release gate (channels), read-only
  forge    blocked, SPLENT_FORGE_TOKEN is not set
           Without it a push, a tag and a release cannot be verified. Run 'splent tokens:setup' for instructions on obtaining tokens.
           To publish to PyPI only, re-run with --no-forge (explicit, one channel).
  pypi     blocked, PyPI credentials are not set
           Missing TWINE_PASSWORD or PYPI_PASSWORD. Run 'splent tokens:setup' for instructions on obtaining tokens.
           To publish to the forge only, re-run with --no-pypi (explicit, one channel).
           splent-io/splent_feature_notes is not on git.splent.io, or not visible without a token. A real release needs it to exist and your token to push to it.
           splent_feature_notes is free on PyPI. A first upload would create the project, permanently, under the account that uploads it.

  Pre-release checks (lint + tests)
  checks   ruff (lint + format)...
  checks   lint OK
  checks   tests...
  ...
  ✅ Test runs (feature × level): 3 passed.
  checks   tests OK

  Contract and package (in a temporary copy)
  contract up to date with the source
  build    building the package in a temporary copy...
  build    splent_feature_notes-1.0.0-py3-none-any.whl  (<size>)
  build    splent_feature_notes-1.0.0.tar.gz  (<size>)
  build    twine check passed, PyPI would accept the metadata
Gate What it checks
Git A release commits the version bump, tags it and pushes both, so the feature must be a git repository with the forge repository as origin. feature:create made yours a repository with the scaffold as its first commit, and what you changed since is not committed. It has no origin yet
Release gate Read-only checks. Who the forge token belongs to and whether it can push, whether PyPI accepts the credentials and the name, and whether the previous release finished. Without credentials it is blocked, and the rehearsal adds what it can find out anyway, here that the repository is not on the forge and the PyPI name is free
Pre-release checks ruff and your tests, as a real release runs them. A failure here stops the release before anything changes
Contract and package The version bump and the contract go into a temporary copy, where the package is built. The wheel and the sdist are the files a real release would upload. Your splent_feature_notes/ is not touched

It ends with the plan and with what stands in the way.

  A real release would
  version  0.0.1 -> 1.0.0 in [project].version of pyproject.toml
  contract refreshed from the source in pyproject.toml
  commit   "chore: release splent-io/splent_feature_notes to v1.0.0", locally, with <n> uncommitted change(s)
  pypi     upload splent_feature_notes 1.0.0 to PyPI (splent_feature_notes-1.0.0-py3-none-any.whl, splent_feature_notes-1.0.0.tar.gz), permanent
  forge    push main to https://git.splent.io/splent-io/splent_feature_notes
  tag      create v1.0.0 and push it to origin
  release  create the release v1.0.0 on git.splent.io
  snapshot .splent_cache/features/splent_io/splent_feature_notes@v1.0.0 (read-only clone of the tag)
  attach   ask whether to pin splent_feature_notes@v1.0.0 in my_first_app/pyproject.toml

  Dry run. Nothing was changed: no file, no commit, no tag, nothing pushed or uploaded.
  A real release would be refused right now:
    - the repository has no 'origin' remote
    - forge: SPLENT_FORGE_TOKEN is not set
    - pypi: PyPI credentials are not set

That is the expected result. Everything that depends only on your code passed (lint, tests, contract and package). What is left is what a real publication needs, a repository on the forge and credentials for both channels. The command exits with status 1 whenever a real release would be refused, so a script can use it as a check.

A real release publishes to PyPI before it pushes anything, because a PyPI version can never be taken back while an unpushed tag costs nothing. If a real release fails halfway, release:resume finishes it without bumping the version.

Optional. Publish for real, under your own name.

The rest of this tutorial, and Tutorials 6 and 7, work on your editable copy of splent_feature_notes. Publish only to try the real thing, under your own name, knowing that a PyPI release is permanent.

  1. Accounts and tokens. You need an account on git.splent.io, where registration is closed, so ask the SPLENT maintainers for one. Create a repository <you>/splent_feature_notes in your namespace. You also need a PyPI account with an API token. splent tokens:setup explains where to get both tokens. Put SPLENT_FORGE_USER, SPLENT_FORGE_TOKEN, TWINE_USERNAME and TWINE_PASSWORD in the workspace .env, and check them with splent check:forge and splent check:pypi.
  2. A PyPI name of your own. PyPI names are global, and the first upload of splent_feature_notes owns that name forever. In splent_feature_notes/pyproject.toml, set [project].name to a name nobody else has, such as splent-feature-notes-<you>. Only the distribution name changes. The import package stays splent_io.splent_feature_notes, and the product keeps calling the feature splent_feature_notes.
  3. The repository. feature:create made the feature a git repository. Commit your work and point origin at your repository.
    cd /workspace/splent_feature_notes
    git add .
    git commit -m "feat: the notes feature"
    git remote add origin ssh://git@git.splent.io:2223/<you>/splent_feature_notes.git
    git push -u origin main
    cd /workspace
    
  4. Rehearse again with the same --dry-run command until it ends with Ready. The same command without --dry-run would release v1.0.0.
  5. Release. Run it without --dry-run, and answer Y when it asks to attach the new version to my_first_app.
    splent feature:release splent-io/splent_feature_notes --bump major
    

    The product now pins splent-io/splent_feature_notes@v1.0.0 from a read-only snapshot in .splent_cache/, and records your PyPI name under [tool.splent.distributions], which the production image in Part 2 installs from. Skip the warning box at the start of Part 2, since your notes stays in the product.

To go back to your editable copy before Tutorial 6, run splent feature:detach splent-io/splent_feature_notes v1.0.0, then splent feature:add splent-io/splent_feature_notes.


Part 2. Deploy to production

This part needs no forge or PyPI account.

The production image installs every feature from PyPI, or from its git tag, at the version the product pins. Your splent_feature_notes was rehearsed, not released, so the image has nothing to install it from. Remove it from the product first.

splent feature:remove splent_feature_notes
  notes removed from features
    uninstalling from web container...
  done.

The product deploys with the other features (auth, public, theme, settings, redis, session_redis and profile). The source stays in /workspace/splent_feature_notes, and Tutorial 6 adds it back. If you published it for real in Part 1, skip this.


Step 3. Build the production image

splent product:build

This can take several minutes the first time. Later builds reuse cached layers.

It checks the product (the UVL configuration, the contracts, every feature pinned and available), writes the deploy files and builds the production image from the product’s own directory. The image installs the framework and the CLI at the versions [tool.splent.runtime] in pyproject.toml pins, and every feature from PyPI, or from its git tag when PyPI does not serve that version. Released features ship their frontend bundles compiled. The final stage is a slim Python image without compilers, Node.js or development extras. product:build has the details.

🏗️  Building deployment artifacts for product: my_first_app

  validate UVL configuration is satisfiable
  contract no conflicts detected
  features  all 7 feature(s) versioned and on PyPI
📦 Generating deploy/.env.example...
✅ Created: /workspace/my_first_app/deploy/.env.example

🐳 Generating deploy/docker-compose.yml...
✅ Created: /workspace/my_first_app/deploy/docker-compose.yml
✅ Created: /workspace/my_first_app/splent.contracts.json

🐳 Building Docker image: my_first_app:0.0.1...
...
✅ Image built: my_first_app:0.0.1

🎯 product:build completed successfully.

When a pinned version is only tagged on the forge, a features line says so (<feature>: not on PyPI, the image will install its git tag <version>) and the image installs the tag, which is the same code.

Most of what it wrote is the deploy bundle in my_first_app/deploy/, everything a server needs, committed with the product.

File Holds
docker-compose.yml The product and every feature service in one stack, with its own project name and network
.env.example Every production setting with its default. The secrets (SECRET_KEY, the database passwords) are empty and marked required, and Compose refuses to start until each is set
README.md How to run it, for whoever runs the server

The build also keeps [tool.splent.runtime] in pyproject.toml current, writes docker/requirements.lock with every third-party package the image installed, and writes splent.contracts.json and docker/spl/ for the image to read. A clone of the repository deploys without SPLENT.

git clone <your repository> && cd my_first_app/deploy
cp .env.example .env    # fill in the required values
docker compose up -d

product:deploy, in the next step, does the same from your workspace and fills in the values for you.


Step 4. Deploy

Production uses the same host ports as development (5591 for the app, plus the database and Redis ports), so stop development first.

splent product:down --dev
...
🛑 Development environment stopped.

If you forget, product:deploy lists the port conflicts and asks Stop and remove the conflicting containers? [y/N]. The default answer stops the deploy.

splent product:deploy

It creates deploy/.env from deploy/.env.example, generates a random SECRET_KEY and asks for the Redis and database passwords, each with a random suggestion, so pressing Enter three times is enough. Then it runs docker compose up -d in deploy/ and waits for the app to answer.

  env      creating deploy/.env from deploy/.env.example
  Value required for REDIS_PASSWORD [<random>]:
  secret   SECRET_KEY generated
  Value required for MARIADB_PASSWORD [<random>]:
  Value required for MARIADB_ROOT_PASSWORD [<random>]:
  deploying my_first_app (prod)...

  health   waiting for app to respond...
  health   ready

  URL: http://localhost:5591
  done.

Open http://localhost:5591. Gunicorn serves the home page from the production image.

The containers come back by themselves after a reboot or a crash, and cookies are marked Secure when the request arrives over HTTPS. Here it arrives over plain HTTP, so signing in below works with no setting. Behind a TLS proxy keep SPLENT_TRUSTED_PROXIES=1, which the product’s template sets, so the app sees the https. See product:deploy.


Step 5. Create the first user

The production database starts empty. deploy/.env sets RUN_DB_SEED=false, so the development test users such as user1@example.com do not exist here. Create the first account with the auth feature’s command.

splent feature:auth create-user admin@example.com --role admin

It asks for the password twice without echoing it.

Password:
Repeat for confirmation:
  Created user admin@example.com with role admin.

While production is deployed, the CLI talks to the production database with the settings in deploy/.env, so the same command you would run in development reaches the right database. Open http://localhost:5591/login and sign in as admin@example.com. splent feature:auth list-users lists the accounts, and splent feature:auth set-password changes a password.

Your product has no admin panel, since Tutorial 1 left the optional admin feature out. The admin role still opens auth’s own accounts screen, so this user sees a Users item in the sidebar, which opens /admin/users to create, edit and deactivate accounts. Other roles matter only to routes that check them with role_required().

The command refuses an email that already has an account. In a script, set AUTH_NEW_USER_PASSWORD instead of answering the prompt, to keep the password out of the shell history. db:seed, db:reset, db:rollback and other commands that rewrite or invent data refuse to run against production.


Step 6. Shut down production

When you are done, stop it.

splent product:down --prod
...
🛑 Production deployment stopped.

The production database lives in a Docker volume, so the account is still there the next time you deploy.


Step 7. Back to development

Start development again.

splent product:up --dev
splent product:run --dev

product:run ends with entrypoint finished once the web server answers. http://localhost:5591 is your development app again, with user1@example.com and 1234.


What you learned

Concept What it means
Feature contract Machine-readable summary of routes, models, hooks, services, and dependencies, inferred from source code
Release rehearsal feature:release --dry-run runs the version, the channel gate, lint, tests and the build, then stops before anything irreversible
Release pipeline One command bumps, publishes to PyPI, then tags and releases on the forge, and snapshots the tag into the cache
Your own PyPI name [project].name is the distribution name. The import package and the product’s reference to the feature do not change
Production deployment A Docker image built from the product alone, the deploy/ bundle any server runs with docker compose up -d, and product:deploy
First user splent feature:auth create-user creates an account in the production database
Dev and prod --dev and --prod keep the environments separate

Next

Continue with Tutorial 6, Extend with refinement, to override a service, extend a model and add routes without forking the base feature.


Back to top

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