Release process¶
femforge and femforge-workbench release together under one version. A release is a version bump and a changelog section merged to main, then a v tag that a workflow turns into PyPI uploads and a GitHub release. The Elmer image and the docs site publish on their own schedules from main. This page covers all three.
Versions¶
Every version string is a static literal, and tests/test_release.py keeps them in lockstep:
| File | Line | Check |
|---|---|---|
pyproject.toml |
version = "X.Y.Z" |
test_package_versions_match |
packages/femforge-workbench/pyproject.toml |
version = "X.Y.Z" |
test_package_versions_match |
src/femforge/__init__.py |
__version__ = "X.Y.Z" |
test_package_versions_match |
pyproject.toml |
the workbench extra pins femforge-workbench==X.Y.Z |
test_workbench_extra_pins_the_workbench_version |
packages/femforge-workbench/pyproject.toml |
the dependency femforge[viz]==X.Y.Z |
test_workbench_depends_on_the_same_femforge_version |
CITATION.cff |
version: X.Y.Z |
test_citation_version_matches |
CHANGELOG.md |
a ## [X.Y.Z] section |
test_changelog_has_release_section |
src/femforge/templates/new/main.py |
the script header pins femforge[workbench]~=X.Y.0 |
test_the_project_template_pins_this_minor_release_and_python |
Set date-released in CITATION.cff to the release date as well. Versions follow Semantic Versioning.
Changelog¶
CHANGELOG.md follows Keep a Changelog 1.1. Pull requests add lines under ## [Unreleased]. At release time, move those lines into a new dated section and update the compare links at the bottom of the file:
## [Unreleased]
## [0.2.0] - 2026-11-20
### Added
- A radiation boundary condition.
[Unreleased]: https://github.com/femforge-io/femforge/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/femforge-io/femforge/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/femforge-io/femforge/releases/tag/v0.1.0
The release workflow copies the section into the GitHub release notes, so write it for users.
Cut a release¶
- Open a pull request that sets the new version in every file of the versions table, moves the Unreleased lines into the new section, and updates the compare links and
date-released. - Run the gates, then merge.
-
Tag the merge commit on
mainand push the tag:
The release workflow¶
Note
.github/workflows/release.yml runs only in the public repository femforge-io/femforge, because its first job checks github.repository.
A tag that matches v[0-9]+.[0-9]+.[0-9]+* starts the workflow. Its jobs run in order, and a failure stops the release:
| Job | What it does |
|---|---|
client |
builds the workbench client with Node 24 and uploads the bundle |
build |
checks that the tag is on main, that both packages carry the tag version (uv version --short --frozen --package), that CHANGELOG.md has the section and that CITATION.cff matches; then builds both packages with uv build --all-packages --no-sources, runs twine check --strict, and installs both wheels into a clean virtual environment to assert femforge.__version__ |
pypi |
uploads the wheels and sdists through PyPI trusted publishing, in the pypi environment, with no stored token |
github-release |
creates the GitHub release for the tag with the changelog section as its notes and the wheels and sdists attached |
A version that holds a letter, such as 0.2.0rc1, becomes a GitHub prerelease.
Elmer image releases¶
The image ghcr.io/femforge-io/femforge-elmer has its own version, IMAGE_VERSION in src/femforge/elmer/runtime/docker_image.py, next to IMAGE_DIGEST, the sha256 of the released Dockerfile. When you change the recipe, raise the version in the same pull request:
The command rewrites both lines when the recipe differs from the released one and does nothing otherwise. --part picks major, minor or patch, the default. It refuses to run from an installed package, since it edits the source.
The Elmer image workflow (.github/workflows/image.yml) runs on a push to main that touches docker_image.py, src/femforge/execution/docker/ or the workflow itself, and on demand. It runs only in femforge-io/femforge:
femforge image tagresolves the tag. A recipe change without a bump yields a-dev.tag, and the job fails.- When GHCR already holds the tag, the job skips the remaining steps. Version tags are immutable.
- It builds the image with
femforge image build --no-pulland runs the Docker end-to-end test through it. - It pushes the image.
- It downloads the source of the installed
elmerfem-cscpackage and attaches it to afemforge-elmer-v<version>GitHub release, which never becomes the latest release.
The base-image workflow (.github/workflows/base-image.yml) runs on the first of each month and on demand. When the digest of ubuntu:24.04 has moved from the one BASE_IMAGE pins, it pins the new digest, runs femforge image bump --part patch, and opens a pull request. That pull request starts no checks by itself; close and reopen it to run them. Merging it publishes the new image.
Docs deploy¶
The deploy job of .github/workflows/docs.yml publishes the site after each push to main in femforge-io/femforge. It takes the site the docs job built and runs npx wrangler@4 deploy with the Cloudflare credentials of the docs environment. wrangler.jsonc names the Worker femforge-docs, serves ./site with its 404 page, and routes docs.femforge.io to it.
The placeholder releases¶
PyPI holds placeholders femforge 0.0.1 and femforge-workbench 0.0.1 with no code in them. Once 0.1.0 is on PyPI, both placeholders are yanked, so an unpinned install never resolves to them.