Release process

Audience: anyone (human or AI agent) committing to vibe-qc, plus users who want to pin a calculation to an exact build. If you only want to install and run, see installation.md.

Branch model

vibe-qc has two long-lived branches with strict roles. All development chats and contributors must respect this split — it’s the mechanism that lets us match a calculation log to the exact code that produced it, and it’s how we keep public users on a stable surface while we iterate.

Branch

Purpose

Push policy

main

Active development. Every feature, bugfix, refactor lands here first. CI must pass. Daily commits expected.

Maintainers + agents, after CI green.

release

Public-facing snapshot. Advertised install instructions and any binary distribution we ever publish pull from release.

Fast-forward only, and only from a tagged commit on main.

Any other branch is a topic branch — short-lived, owned by a single contributor or chat session, gets squashed or rebased into main once its work lands.

Note

Pre-v0.4.0 deviation. Until the first public tag (planned for v0.4.0), the docs site at vibe-qc.com tracks main directly, not release. The reasoning: holding the public docs at v0.1.0 while v0.2.0 / v0.3.0 / v0.4.0-dev features land on main would mislead new readers into a months-stale install matrix and tutorial set. The site banner reads dev 0.4.0.dev0 (main @ <sha>) during this window so it’s still unambiguous which build a calculation log came from. Starting with the v0.4.0 tag, the site switches to the release-branch model described above and the banner reads Release v0.4.0.

What’s the user-visible difference?

Every vibe-qc run prints a banner that identifies the build:

╔════════════════════════════════════════════════════════════════════════╗
║ Release v0.1.0  —  Quantum chemistry for molecules and solids          ║
║ © Michael F. Peintinger · MPL 2.0  ·  https://vibe-qc.com              ║
║ linked: libint 2.13.1 · libxc 7.0.0 · spglib 2.7.0                     ║
╚════════════════════════════════════════════════════════════════════════╝

vs. a development build:

╔════════════════════════════════════════════════════════════════════════╗
║ dev 0.1.0 (main @ abc1234)  —  Quantum chemistry for molecules and …   ║
║ © Michael F. Peintinger · MPL 2.0  ·  https://vibe-qc.com              ║
║ linked: libint 2.13.1 · libxc 7.0.0 · spglib 2.7.0                     ║
╚════════════════════════════════════════════════════════════════════════╝

The first form appears only when:

  1. The current commit is exactly tagged (e.g. v0.1.0).

  2. The working tree is clean (no uncommitted changes).

Any other state — branch, dirty tree, detached HEAD, untagged commit — shows the dev form, including branch name, short SHA, and a dirty flag if there are uncommitted changes.

Cutting a release — checklist

Maintainers only. Run from a clean checkout of main with everything committed and pushed.

  1. Pre-flight

    git fetch origin
    git checkout main
    git pull --ff-only
    .venv/bin/python -m pytest tests/                 # 100 % pass
    .venv/bin/python -c "from vibeqc.banner import build_info; print(build_info())"
    

    The build_info() output should show dirty: False. If anything else is wrong, fix it on main first.

  2. Bump the version

    Edit version in pyproject.toml (and only that — the banner reads this). Commit:

    git commit -am "release: vX.Y.Z"
    git push origin main
    
  3. Tag the release commit on main

    git tag -a vX.Y.Z -m "vibe-qc X.Y.Z"
    git push origin vX.Y.Z
    
  4. Fast-forward release to that tag

    git checkout release       # or git checkout -b release if first time
    git merge --ff-only vX.Y.Z
    git push origin release
    

    If git refuses the fast-forward, stop. release has somehow diverged from main’s tag. Investigate before doing anything destructive.

  5. Verify on a clean checkout

    cd /tmp && rm -rf vibeqc-rel-check
    git clone -b release https://gitlab.peintinger.com/mpei/vibeqc.git vibeqc-rel-check
    cd vibeqc-rel-check
    ./scripts/setup_native_deps.sh
    python3 -m venv .venv && .venv/bin/pip install -e '.[test]'
    .venv/bin/python -c "from vibeqc.banner import print_banner; print_banner()"
    

    Banner should read Release vX.Y.Z.

  6. Post-release

    • Update the changelog with the release notes (sourced from the project-root CHANGELOG.md).

    • Bump the next 0.X.Y+1.dev0 version on main so subsequent dev builds are visibly post-release.

    • The vibe-qc.com docs site auto-deploys via GitLab CI on every push to main or release. The push of release in step 4 already triggered a deploy; the next pipeline at https://gitlab.peintinger.com/mpei/vibeqc/-/pipelines should show green within ~3 minutes and the live site banner should read Release vX.Y.Z. For the v0.4.0 cutover specifically, edit .gitlab-ci.yml to tighten the only: [main, release] filters to only: [release] — this stops dev pushes to main from overwriting the publicly-advertised tagged content. Until that edit, both branches publish on push; the most-recent push wins.

Running calculations against a specific build

Common case: testing a fix from a topic branch, comparing accuracy across builds, or reproducing an old result. The banner is the source of truth, so:

# 1. Clone or check out the exact ref you want to test.
git fetch origin
git checkout release         # or v0.1.0, or some-topic-branch

# 2. Rebuild the native deps and the python package against that tree.
./scripts/setup_native_deps.sh
.venv/bin/pip install -e '.[test]' --force-reinstall --no-deps

# 3. Confirm the banner shows what you expect.
.venv/bin/python -c "from vibeqc.banner import print_banner; print_banner()"

# 4. Run your calculation. Every persisted SCF log carries the same
# banner at the top, so the result is unambiguously paired to the build.

If a calculation gives a wrong answer, always include the banner in the bug report. The branch+SHA pinpoint the exact source state; the linked-library line pinpoints the native ABI.

CI hooks

What’s wired today (.gitlab-ci.yml at repo root):

  • Docs build + deploy runs on every push to main or release. Container: python:3.13-slim for build, alpine:latest for deploy. Runner: erzherzog-docker (gitlab/gitlab-runner in Docker on the same host as the GitLab CE instance, tags docker/sphinx). Deploy uses an rsync-over-SSH push to web18@vibe-qc.com:/web/, with the ed25519 deploy key stored as the masked, protected $DEPLOY_SSH_KEY_B64 CI variable. Build logs surface in GitLab’s pipeline UI.

What’s planned, not yet wired:

  • release is a protected branch on the GitLab project: only fast-forward from a tag is allowed (push rule). Apply this in the GitLab UI before the v0.4.0 tag.

  • A nightly job re-runs the test suite against release to catch silent breakage from environment drift (e.g. a newer compiler rejecting our C++). Add as a scheduled pipeline once the test suite runs cleanly inside the CI container (currently needs the C++ toolchain image, which the docs pipeline doesn’t exercise).

  • A manual job tags + pushes the release branch given a tag name. Useful for one-click cutting of a release; today the workflow is the manual step-1-through-6 checklist above.

Why not just use main for everything?

We’re a single-developer project today, so the temptation is real. The reason release exists anyway is that we’re going public soon and the docs site at https://vibe-qc.com pulls from a fixed branch (currently main, soon release). Keeping release separate means casual visitors see stable docs and stable install instructions even as we tear apart main for a refactor.