This page is aimed at people developing Thetis, maintaining continuous
integration (CI), and doing releases.
Branches (main vs release)
Thetis has two long-lived branches:
main is the development branch. New features should be developed here.
Thetis main tracks Firedrake main and generally requires Firedrake
main.
release is the stable branch intended for users. It is kept compatible
with the latest stable Firedrake release.
The temporary branch release-candidate is used only while preparing a new
Thetis release after a Firedrake release. It is force-updated to the intended
main release commit, and GitHub Actions tests it against the same Firedrake
dev-release Docker image used for release. This gives maintainers a
tested candidate before the public release branch is force-updated and
tagged.
CI reflects this by testing main against the Firedrake dev-main Docker
image and testing release and release-candidate against the Firedrake
dev-release Docker image.
Maintaining release
Policy (Hard Reset at Firedrake Release Points)
Thetis follows Firedrake’s major release cadence:
main is the normal development branch. New features and ordinary bug fixes
should land on main.
release is the current stable branch. It tracks the latest stable
Firedrake release and is the branch used for the published website.
When Firedrake makes a major release, Thetis release is hard-reset
to a main commit that has been tested in the Firedrake dev-release
container.
Between these reset points, keep release conservative. Bug fixes may be
cherry-picked when they should be available on the stable line, but do not
backport new functionality, API changes, substantial rewrites, or changes
whose risk is out of proportion to the bug being fixed.
After Firedrake has made a major release, avoid merging further changes to
Thetis main until the corresponding Thetis release has been completed. The
release should be based on the main state that is intended to work with the
new Firedrake release, not on later development work against a moving Firedrake
main.
This policy keeps main linear and avoids merge commits whose only purpose is
to move changes back and forth between long-lived branches. The hard reset is
also acceptable because users will typically need to reinstall PETSc and
Firedrake at each major release cycle anyway. The tradeoff is that the public
release branch is force-updated at release points. Tags are therefore the
stable install points for historical releases.
When Does release Move?
release moves in three cases:
Firedrake makes a major release and Thetis release is hard-reset
to the compatible, tested main commit.
A release-only fix is required for the current stable Firedrake stack.
A shared fix from main must be copied to the current stable branch before
the next hard reset.
Keep release changes minimal and compatibility-driven. Each update to
release creates a new installable stable state and should be tagged after
the corresponding CI run has passed.
Workflow: Normal Development and Bug Fixes
Most changes should go to main only:
Create a branch from main.
Open a PR with base branch main.
Merge according to normal project practice.
If the change does not need to ship on the current stable Firedrake stack, do
not backport it. It will arrive on release at the next hard reset.
Workflow: Shared Fix from main to release
Use this only when a fix already on main must also ship on the current
stable branch before the next reset.
Create a backport branch from release and cherry-pick the main fix:
git fetch origin
git checkout -b release-backport/<short-description> origin/release
git cherry-pick -x <main-fix-sha>
git push -u origin release-backport/<short-description>
Open a PR with base branch release.
Merge with a method that leaves a single release-side fix commit where
possible (squash merge, or rebase merge if enabled). If using squash merge,
keep the original cherry picked from line or mention the source main
PR/commit in the squash commit message.
Wait for the post-merge release push workflow to pass.
Tag the resulting release commit using the tag convention below.
The release-side commit will usually have a different SHA from the main fix.
This is expected because the parent commits differ; the same patch cannot have
the same commit SHA unless it has the same parents and metadata. If release
was already N development commits behind main before the cherry-pick,
then after this workflow release is normally N + 1 commits behind
main and one commit ahead of main.
Workflow: release-Only Fix
Use this when the issue only exists on the current stable branch, for example
because of the older Firedrake dependency stack.
Create a branch from release:
git fetch origin
git checkout -b release-fix/<short-description> origin/release
Commit the fix and open a PR with base branch release.
Merge with a method that leaves a single release-side fix commit where
possible.
Wait for the post-merge release push workflow to pass.
Tag the resulting release commit using the tag convention below.
If the same fix is also needed on main, put it on main first and use the
shared-fix workflow above. For a true release-only fix, if release was
already N development commits behind main before the fix, then after the
fix release is normally N commits behind main and one commit ahead of
main.
Workflow: Hard-Reset release to main
This is the normal workflow when Firedrake makes a major release and
Thetis release should become the compatible state from main. The goal is
to tag only a commit that has passed Thetis CI in the Firedrake release
container.
Before moving release, check whether the final state of the old stable line
already has the intended tag. If it does not, preserve it with a tag using the
tag convention below.
git fetch origin
git checkout release
git pull --ff-only
git tag -a <old-version-final-or-patch-tag> release -m "<tag message>"
git push origin <old-version-final-or-patch-tag>
Prepare and test a temporary release-candidate branch. Pushes to this branch
run the same push.yml workflow as release and use the Firedrake
dev-release Docker image.
git fetch origin
git checkout -B release-candidate <main-sha-or-origin/main>
git push --force-with-lease origin release-candidate
Wait for the release-candidate push workflow to pass. If it fails, fix the
problem on release-candidate and push again until CI passes. Keep
release-candidate based on the selected main commit; it is fine to
rewrite or squash fix commits on release-candidate while preparing the
release.
If fixes were needed on release-candidate, fast-forward main to the
passing candidate before moving release:
git fetch origin
git checkout main
git pull --ff-only
git merge --ff-only origin/release-candidate
git push origin main
This push to main should still be a normal fast-forward push, not a
force-push. If GitHub rejects this due to branch protection rules, a
maintainer must temporarily relax the relevant rule, make the normal
fast-forward push, and restore branch protection immediately afterwards.
After the Thetis release is complete, fix any remaining main CI failure
through the normal PR workflow.
After release-candidate has passed, move release to exactly the passing
commit:
git fetch origin
git checkout release
git reset --hard origin/release-candidate
git push --force-with-lease origin release
Wait for the release push workflow to pass, then tag that exact commit and
push the tag. If the release workflow fails unexpectedly, do not tag it; fix
the problem on release-candidate and repeat the move to release. The
package version is derived from git tags, so no version-only commit is needed at
the hard reset point.
git tag -a <new-release-version> release -m "Thetis <new-release-version>"
git push origin <new-release-version>
Immediately restore/confirm the branch protection settings after the force-push
if they had to be relaxed.
History Shape
Immediately after a hard reset, release contains the vetted main state.
If release receives a release-only fix, it will diverge from main until
the next reset:
main: A -- B -- C -- D
release: A -- B -- R1
After the reset, active release contains the selected main state and the
old state remains available by the tag created before the reset:
main: A -- B -- C -- D
release: A -- B -- C -- D
tag: A -- B -- R1
Publishing A Tagged Release
When publishing a tagged release, push a tag, create a GitHub Release, and
verify Zenodo archived the release (see the Zenodo section below). If the
release includes documentation changes, update the published website at the same
time.
Tag convention
The repository currently contains multiple historical tag naming schemes (for
example 2026.4.0 / 2025.10.1 as well as older Thetis_YYYYMMDD-style
tags). For new releases, the release tag is the source of the packaged version,
so use PEP 440-compatible tags.
New stable release tags should use:
where YYYY.M is the Firedrake major release series used by that Thetis
release branch and N is incremented for each Thetis release update on that
branch. For example, the first Thetis release against the Firedrake 2026.4
line should be tagged 2026.4.post0. The next Thetis update to that release
line should be tagged 2026.4.post1, regardless of whether the compatible
Firedrake stack is 2026.4.0, 2026.4.1, or a later Firedrake patch. This
keeps Thetis release numbering visibly separate from Firedrake patch numbering.
Tags are normally created only:
before a hard reset, if the final installable state of the old stable line has
not already been tagged;
after a hard reset, after CI has passed for the new Thetis release against the
Firedrake release container; or
after a tested release-only fix or backported bug fix has been merged to
release.
Do not use tags such as 2026.4.rel1 for Thetis releases: the release tag is
used as the Python package version, and rel1 is not valid in that position
under PEP 440.
Python package versions
Thetis uses dynamic versioning from git tags and branch state. Developers
normally only choose the release tags described above; untagged development
versions are derived automatically.
The automatic package-version rules are:
A tagged commit has the exact release version from the tag.
Untagged commits on main and ordinary feature branches report
YYYY.M.devN. YYYY.M is based on the commit date of HEAD (the Git
committer date, not the author date), and N is the number of commits in
that month.
Untagged commits on release and release-*/release/* maintenance
branches, including release-candidate, report the next postN
development version for that release line.
If main and release point at the same tagged commit, they report the
same exact version. Once main receives new commits, it reports a development
version again. Do not manually tag arbitrary main commits to create
development versions.
For editable installs, git pull updates the source code but does not rewrite
the installed Python distribution metadata that commands such as pip list
and pip show read. Re-run pip install -e . after switching branches,
pulling new commits, or creating/checking out a release tag if the version shown
by pip needs to match the current checkout.