Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: CI

on:
push:
branches: [master, prerelease]
pull_request:
branches: [master, prerelease]

jobs:
build-and-test:
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Set up conda (Miniforge)
uses: conda-incubator/setup-miniconda@v3
with:
python-version: ${{ matrix.python-version }}
miniforge-variant: Miniforge3
use-mamba: true
auto-activate-base: false
activate-environment: test-env

- name: Install llvm-openmp (macOS)
if: runner.os == 'macOS'
shell: bash -el {0}
run: conda install -y -c conda-forge llvm-openmp

- name: Install svmbir with dev dependencies
shell: bash -el {0}
run: pip install -e ".[dev]"

- name: Run tests
shell: bash -el {0}
run: pytest svmbir/tests/ -v
40 changes: 40 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Build and Release Wheels

on:
push:
tags:
- "v*"

jobs:
create-draft-release:
name: Create draft release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: softprops/action-gh-release@v2
with:
draft: true
generate_release_notes: true

build-linux:
name: Linux wheels and sdist
runs-on: ubuntu-latest
needs: create-draft-release
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Build wheels
uses: pypa/cibuildwheel@v2.22.0

- name: Build sdist
run: pipx run build --sdist

- name: Upload to draft release
run: gh release upload ${{ github.ref_name }} wheelhouse/*.whl dist/*.tar.gz --clobber
env:
GH_TOKEN: ${{ github.token }}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ sftp-config.json
*.DS_Store

# Pycharm idea folder
.idea/
.idea/
# cibuildwheel output
wheelhouse/
93 changes: 0 additions & 93 deletions .travis.yml

This file was deleted.

114 changes: 114 additions & 0 deletions dev_scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
Considerations for package maintenance:
---------------------------------------

1. Package metadata (requires-python, dependencies in pyproject.toml)
This declares the minimum you need, not the maximum. The strong community consensus is: never put upper bounds on requires-python or on dependencies in published package metadata. Upper bounds are actively harmful — they prevent users from installing your package alongside other packages that have already moved forward. If numpy>=2.0 breaks something, the right fix is to fix the code, not to block numpy>=2.0. The only exception is when you have a known, tested incompatibility with a specific version.

2. The CI test matrix
CI (Continuous Integration) is automated testing that runs in the cloud every time code is pushed to the repository or a pull request is opened. For svmbir, this means GitHub automatically installs the package and runs the test suite on Linux across all supported Python versions. The goal is to catch breakage early — before a bug reaches users — and to confirm that changes work correctly across the Python versions the package claims to support. Check https://github.com/cabouman/svmbir/actions for status.

The CI matrix is the list of Python versions CI tests against. The current matrix is under jobs->matrix->python-version in `ci.yml`. When a new Python version comes out in October, add it to the list and see if anything breaks. This is the right place to discover problems early, before users file bugs.

3. The dev environment (install_conda_environment.sh)
This should pin to a specific recent version for a reproducible daily-driver environment. The version you pick here doesn't limit what users can run — it's just what developers work in. Updating this once a year when a new Python ships is reasonable.

4. The CI workflow (.github/workflows/ci.yml)
This is the configuration file that tells GitHub how to run CI. It specifies which Python versions to test, which operating system to use (Linux only — see section 5 for macOS), and what commands to run (install the package, run pytest). It fires automatically on every push to `master` or `prerelease`, and on every pull request targeting either branch — no manual action needed.

Updating Python versions (do this ~once a year):
- **Add a new version**: Each October, Python ships a new release. Add it to the `python-version` list in `ci.yml`, and add the matching `cp3XX-*` entry to the `build` setting in `[tool.cibuildwheel]` in `pyproject.toml` so release wheels are built for it too. Note that Cython may lag a few months behind a new Python release, so if CI fails on the new version due to a Cython build error, simply remove it from both lists and try again after the next Cython release.
- **Drop an EOL version**: Python versions reach end-of-life roughly 3 years after release (schedule at python.org/downloads). Remove it from the `python-version` list in `ci.yml`, the `build` setting in `pyproject.toml`, and raise `requires-python` in `pyproject.toml` to match.

Updating OS runners:
- CI runs Linux only (`ubuntu-latest`). macOS is no longer in the CI matrix — macOS compatibility is verified when building and test-installing the release wheels locally (see section 5 below).
- If a future Linux runner name change is needed, `ubuntu-latest` tracks GitHub's current default and rarely requires manual updates.

5. The release workflow (.github/workflows/release.yml) and local macOS build
Linux wheels and the source distribution are built automatically by GitHub Actions. macOS arm64 wheels are built locally using `dev_scripts/build_mac_wheels.sh` and uploaded to the same draft release. Intel Mac (x86_64) is no longer supported.

New releases:
-------------

Each release is staged on the `prerelease` branch and stays as a non-public draft until you explicitly publish it, so you can verify everything before it goes live. The process has two steps: test first, then release.

Both scripts are run from the `prerelease` branch. The typical sequence after your feature branch PR has been merged into `prerelease`:

```
git checkout prerelease && git pull
./test_release.sh # dry run — verify the pipeline works
./cut_release.sh # real release
```

`test_release.sh` warns (but does not exit) if you are not on `prerelease`, so it can also be used from a feature branch to debug a workflow problem.

Note that the scripts need to be run as `./<script>.sh` rather than `source <script>.sh`.

Step A — Test the release workflow (run before every release):
`./test_release.sh` automates a complete dry run of the release process using a throwaway tag. It:
- Creates and pushes a test tag of the form `v<current_version>-bump-test` (e.g. `v0.4.0-bump-test`), based on the current version in `pyproject.toml`. The wheels built during the test will carry the current version number — this is expected. The real release will update `pyproject.toml` to the new version before building.
- Triggers GitHub Actions to create a draft release and build Linux wheels
- Builds the macOS arm64 wheels locally and uploads them to the same draft release
- Pauses for you to verify that all wheels appear correctly on the GitHub Releases page
- Cleans up the test tag and draft release automatically when you confirm success

Step B — Cut the real release:
Once the test in Step A passes, `./cut_release.sh` does the real release. It:
- Shows the current version and the latest published release, and prompts for the new version
- Validates the new version is greater than the existing one
- Updates `pyproject.toml`, commits, pushes, and creates the version tag
- Triggers GitHub Actions to create a draft release and build Linux wheels
- Builds the macOS arm64 wheels locally and uploads them
- Prints the remaining manual steps: verify the draft release, open the prerelease→master PR, and publish

Prerequisites: `cibuildwheel` and `gh` (the GitHub CLI) are installed automatically by `install_conda_environment.sh`. `gh` is GitHub's official command-line tool — it talks to the GitHub API to create releases and upload wheel files on your behalf, separate from your normal `git` push access. Both scripts detect whether `gh` is authenticated and run `gh auth login` for you if needed (opens a browser to log in to your GitHub account).

Manual steps (for reference or recovery if a script fails partway through):
1. On the `prerelease` branch, update the version in `pyproject.toml` to the new version (e.g. `0.4.X`). This is the single source of truth — `__init__.py` reads it at runtime via `importlib.metadata`.
2. Commit and push the version bump: `git commit -m "Release v0.4.X" && git push`
3. Tag the commit and push the tag: `git tag v0.4.X && git push --tags`
GitHub Actions immediately creates a draft release, then builds Linux wheels and the source distribution and uploads them.
4. Build and upload the macOS arm64 wheels: `cd dev_scripts && ./build_mac_wheels.sh v0.4.X`
5. Go to the repo's Releases page on GitHub and confirm both Linux and macOS wheels are attached.
6. Merge `prerelease` → `master` via a pull request: `gh pr create --base master --title "Release v0.4.X"`
7. After the PR merges, publish the draft release on GitHub.

Updating the release workflow over time:
- **Python versions**: keep the `build` setting in `[tool.cibuildwheel]` in `pyproject.toml` in sync with the CI matrix in `ci.yml` and `requires-python`. All three should agree.
- **cibuildwheel version**: `pypa/cibuildwheel@v2.22.0` in `release.yml` is pinned for reproducibility. When a new Python version requires a newer cibuildwheel release, bump the pin here and also update the version in `install_conda_environment.sh` so local builds stay in sync.

Adding PyPI distribution later:
When ready to publish to PyPI, add a final job to `release.yml` after `create-draft-release`:
1. Set up PyPI Trusted Publishing in your PyPI project settings (links the GitHub repo without needing a stored API token).
2. Add a `publish-to-pypi` job that downloads the `dist/` artifacts and runs `pypa/gh-action-pypi-publish`.


Development workflow: prerelease → master
-----------------------------------------

The standard workflow for this repo is: feature branch → PR to `prerelease` → PR from `prerelease` to `master`. CI runs automatically at both gates.

When working on a feature branch:
1. Push your branch to GitHub: `git push -u origin <branch-name>`
2. Open a pull request to `prerelease`: `gh pr create --base prerelease`
3. GitHub automatically runs CI. Go to the PR page and click the "Checks" tab, or visit https://github.com/cabouman/svmbir/actions, to watch the jobs run (one per Python version on Linux).
4. If a job fails, click into it to read the log, fix the issue, push another commit to the branch, and CI re-runs automatically.
5. Once CI passes, merge the PR into `prerelease`.
6. When ready to release, run `./test_release.sh` then `./cut_release.sh` from dev_scripts/ on the `prerelease` branch.
7. Open a PR from `prerelease` to `master` and merge after CI passes.
8. Publish the draft release on GitHub.


Keeping current over time — the practical options:
--------------------------------------------------

1. Dependabot (built into GitHub): opens automated PRs when dependencies release new versions. Very low friction — it just creates a PR, and your CI tells you if it breaks anything. This is the right tool for routine package bumps.

2. Manual annual review: less automated but sufficient for a project that doesn't change often. When a new Python or NumPy ships, run the test suite against it and fix what breaks.

3. pip-compile / lock files: great for applications that need exact reproducibility, but not the right tool for a library — it over-constrains what users can install alongside you.

For this project specifically, Cython + C extensions have two real compatibility risks:

1. New Python versions: Cython generates standard CPython API code, so new Pythons almost always Just Work once Cython itself is updated. The main friction is that Cython needs to release a version supporting the new Python's ABI before you can build wheels for it.

2. NumPy major versions: NumPy 2.0 changed the C API in breaking ways. We already added NPY_NO_DEPRECATED_API which is the right first step, but a runtime test against NumPy 2.x is worth adding to the CI matrix.
82 changes: 82 additions & 0 deletions dev_scripts/build_mac_wheels.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/bin/bash
# Build macOS arm64 wheels locally and upload them to the draft GitHub Release
# for the given tag.
#
# Prerequisites:
# pip install cibuildwheel (once; any Python environment)
# gh auth login (once; authenticates the gh CLI)
#
# Usage (run from dev_scripts/):
# ./build_mac_wheels.sh v0.4.1

set -euo pipefail

TAG=${1:?"Usage: build_mac_wheels.sh <tag> e.g. ./build_mac_wheels.sh v0.4.1"}

# Always work from the repo root regardless of where the script is called from.
cd "$(dirname "$0")/.."

# Fetch tags from remote so we can verify the tag exists.
echo "Fetching tags ..."
git fetch --tags --quiet

# Verify the tag exists.
git rev-parse "$TAG" >/dev/null 2>&1 || {
echo "ERROR: Tag $TAG not found on remote. Push the tag first:"
echo " git tag $TAG && git push --tags"
exit 1
}

# Verify HEAD matches the tag so the wheel is built from the correct commit.
HEAD_COMMIT=$(git rev-parse HEAD)
TAG_COMMIT=$(git rev-parse "$TAG")
if [ "$HEAD_COMMIT" != "$TAG_COMMIT" ]; then
echo "ERROR: HEAD does not match tag $TAG."
echo "Check out the tagged commit first:"
echo " git checkout $TAG"
exit 1
fi

# Verify the working tree is clean.
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "ERROR: Working tree has uncommitted changes."
echo "Commit or stash all changes before building a release wheel."
exit 1
fi

# Verify the git submodule is initialized.
if ! ls svmbir/sv-mbirct/src/*.c >/dev/null 2>&1; then
echo "ERROR: C source files not found — git submodule not initialized."
echo "Run: git submodule update --init --recursive"
exit 1
fi

# Verify cibuildwheel is available.
if ! command -v cibuildwheel &>/dev/null; then
echo "ERROR: cibuildwheel not found."
echo "Install it with: pip install cibuildwheel"
exit 1
fi

# Verify the gh CLI is authenticated; offer to log in if not.
if ! gh auth status &>/dev/null; then
echo "gh CLI not authenticated — launching 'gh auth login' now ..."
echo "(This is a one-time step. Follow the prompts to log in via browser.)"
gh auth login
if ! gh auth status &>/dev/null; then
echo "ERROR: gh auth login did not succeed. Re-run this script to try again."
exit 1
fi
fi

echo "Building macOS arm64 wheels for $TAG ..."
cibuildwheel --platform macos

echo "Uploading wheels to draft release $TAG ..."
gh release upload "$TAG" wheelhouse/*.whl --clobber

echo ""
echo "macOS wheels uploaded. Check that the Linux wheels are also attached, then"
echo "publish the release at:"
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
echo " https://github.com/$REPO/releases/tag/$TAG"
2 changes: 1 addition & 1 deletion dev_scripts/clean_svmbir.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ cd ..
/bin/rm -r dist
/bin/rm -r svmbir.egg-info

pip uninstall svmbir
pip uninstall -y svmbir
cd dev_scripts
Loading
Loading