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
23 changes: 20 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
name: ALPS CI/CD

on: [push, pull_request]
on:
push:
branches:
- master
tags:
- 'v*'
pull_request:
branches:
- master

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
linux-build:
name: Build ALPS on ${{ matrix.plat.os }}
name: >-
ALPS / ${{ matrix.plat.os }} / ${{ matrix.plat.c_compiler }}-${{ matrix.plat.c_version }} /
Python ${{ matrix.plat.py_version }} / Boost 1.${{ matrix.plat.boost_version }} /
C++${{ matrix.plat.cxx_standard || '17' }}
runs-on: ${{ matrix.plat.os }}
# Worst observed healthy job is ~60 min cold; a hang otherwise burns the
# 6-hour GitHub default.
Expand Down Expand Up @@ -110,7 +125,9 @@ jobs:
cmake --build build -j 2 -t test

macos-build:
name: Build ALPS on ${{ matrix.plat.os }} / ${{ matrix.plat.c_compiler }}
name: >-
ALPS / ${{ matrix.plat.os }} / ${{ matrix.plat.c_compiler }} /
Python ${{ matrix.plat.py_version }} / Boost 1.${{ matrix.plat.boost_version }} / C++17
runs-on: ${{ matrix.plat.os }}
# Intel runners are slow and variable; worst observed healthy job ~85 min.
timeout-minutes: 120
Expand Down
58 changes: 35 additions & 23 deletions .github/workflows/build_wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,40 @@ name: ALPS Python Packaging CI/CD
on:
push:
branches:
- master # the default branch
- master
tags:
- 'v*' # this triggers the workflow when a tag starting with 'v' is pushed
- 'v*'
pull_request:
branches:
- master

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
check_version:
name: Check release version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: '3.13'
- run: python -m pip install packaging pytest
- name: Test release validation
run: python -m pytest test/packaging -q
- name: Check package, SDK, and tag versions
run: python script/check_release_version.py

build_wheels:
name: Build wheels on ${{ matrix.plat.os }}
needs: [check_version]
runs-on: ${{ matrix.plat.os }}
strategy:
matrix:
# macos-13 is an intel runner, macos-14 is apple silicon
plat:
plat:
- { os: ubuntu-latest, target: "", arch: x86_64, homebrew: ''}
#- { os: macos-13, target: "13.0" , arch: x86_64, homebrew: '/usr/local'} #DEPRECATED. Too old.
- { os: macos-15, target: "15.0" , arch: arm64, homebrew: '/opt/homebrew'}
- { os: macos-26, target: "26.0" , arch: arm64, homebrew: '/opt/homebrew'}

Expand All @@ -41,9 +58,9 @@ jobs:
fi

- name: Build wheels
uses: pypa/cibuildwheel@v2.22.0
uses: pypa/cibuildwheel@v4.2.0
env:
CIBW_BUILD: cp39-* cp310-* cp311-* cp312-* cp313-*
CIBW_BUILD: cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*
CIBW_ARCHS: ${{ matrix.plat.arch }}
CIBW_ARCHS_MACOS: ${{ matrix.plat.arch }}

Expand All @@ -55,15 +72,6 @@ jobs:
Boost_ROOT_DIR=/Users/runner/work/ALPS/ALPS/boost_1_87_0
MACOSX_DEPLOYMENT_TARGET=${{ matrix.plat.target }}
CXXFLAGS="-stdlib=libc++"
# CIBW_ENVIRONMENT: >

# env:
# CIBW_SOME_OPTION: value
# ...
# with:
# package-dir: .
# output-dir: wheelhouse
# config-file: "{package}/pyproject.toml"

- uses: actions/upload-artifact@v7
with:
Expand All @@ -73,6 +81,7 @@ jobs:

build_sdist:
name: Build source distribution
needs: [check_version]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand All @@ -91,19 +100,22 @@ jobs:
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write
#if: github.event_name == 'release' && github.event.action == 'published'
# or, alternatively, upload to PyPI on every tag starting with 'v' (remove on: release above to use this)
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'release'
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: '3.13'
- run: python -m pip install packaging
- uses: actions/download-artifact@v8
with:
# unpacks all CIBW artifacts into dist/
pattern: cibw-*
path: dist
merge-multiple: true

- name: Check every distribution before publishing
run: python script/check_release_version.py --dist dist

- uses: pypa/gh-action-pypi-publish@release/v1
#with:
# To test:
# repository-url: https://test.pypi.org/legacy/
2 changes: 1 addition & 1 deletion ALPS_VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.3.4
3.0.0
32 changes: 32 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Contributions at every level — from a one-line bug report to a new simulation
- [Getting started with the code](#getting-started-with-the-code)
- [Making a change](#making-a-change)
- [Submitting a pull request](#submitting-a-pull-request)
- [Preparing a release](#preparing-a-release)
- [Review process](#review-process)
- [Code style](#code-style)
- [Recognition](#recognition)
Expand Down Expand Up @@ -147,6 +148,37 @@ For substantial changes — new simulation applications, new libraries, signific

---

## Preparing a release

Update both `ALPS_VERSION.txt` (the C++ SDK version) and `[project].version`
in `pyproject.toml` before creating a release tag. For a final release, both
must be `X.Y.Z` and the tag must be `vX.Y.Z`. For a prerelease such as
`vX.Y.Z-beta.1`, keep the SDK core at `X.Y.Z` and use the Python version
`X.Y.Zb1`. The other supported tag suffixes are `alpha.N`, `rc.N`, and `dev.N`.

Validate the intended tag locally using Python 3.11 or newer:

```bash
python -m pip install packaging
python script/check_release_version.py --ref refs/tags/vX.Y.Z
```

The packaging workflow checks these versions before building and checks every
wheel and source distribution, including its embedded metadata, before upload.
Tag pushes publish the full release to PyPI, including CPython 3.9–3.14 wheels.
Merge and validate the release commit before tagging it. Keep tags fixed once
their release has been published.

If a published tag contains the wrong version, rerunning its workflow will
rebuild the same incorrect artifacts. Correct both version files first. If
the intended version has no distributions on PyPI, maintainers can approve
resetting the tag to the validated correction and publishing that version.
If the intended version already has distributions, prepare a new patch
release instead: PyPI does not allow replacing uploaded filenames. Do not
use `skip-existing` to hide a version mismatch.

---

## Review process

ALPS uses a consensus-based review model:
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ cmake.define.ALPS_PYTHON_WHEEL = "ON"

[project]
name = "pyalps"
version = "2.3.4b1"
version = "3.0.0"
authors = [
{ name="Sergei Iskakov", email="siskakov@umich.edu" },
{ name="Fei Lin", email="feilin.physics@gmail.com" }
Expand All @@ -50,6 +50,7 @@ classifiers = [
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: Implementation :: CPython',
"Operating System :: POSIX",
Expand Down
110 changes: 110 additions & 0 deletions script/check_release_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Check release versions before building or publishing (Python >= 3.11)."""

import argparse
from email.parser import BytesParser
import os
from pathlib import Path
import re
import tarfile
import tomllib
import zipfile

from packaging.utils import (
canonicalize_name,
parse_sdist_filename,
parse_wheel_filename,
)
from packaging.version import Version


CORE_PATTERN = r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)"
TAG_PATTERN = rf"v{CORE_PATTERN}(?:-(?:alpha|beta|rc|dev)\.[0-9]+)?"


def check_version(root: Path, ref: str) -> Version:
core = (root / "ALPS_VERSION.txt").read_text().strip()
if not re.fullmatch(CORE_PATTERN, core):
raise ValueError("ALPS_VERSION.txt must contain MAJOR.MINOR.PATCH")

with (root / "pyproject.toml").open("rb") as stream:
project = tomllib.load(stream)["project"]
version = Version(project["version"])
if version.release != Version(core).release:
raise ValueError(
f"pyproject.toml version {version} disagrees with ALPS_VERSION.txt ({core})"
)

if ref.startswith("refs/tags/"):
tag = ref.removeprefix("refs/tags/")
if not re.fullmatch(TAG_PATTERN, tag):
raise ValueError(
f"Invalid release tag {tag!r}; expected vMAJOR.MINOR.PATCH"
"[-{alpha,beta,rc,dev}.N]"
)
if Version(tag) != version:
raise ValueError(
f"Release tag {tag} disagrees with pyproject.toml ({version})"
)

return version


def check_distributions(directory: Path, expected: Version) -> None:
artifacts = sorted(directory.iterdir())
if not artifacts:
raise ValueError(f"No distributions found in {directory}")

for artifact in artifacts:
if artifact.suffix == ".whl":
name, version, _, _ = parse_wheel_filename(artifact.name)
with zipfile.ZipFile(artifact) as archive:
members = [
member for member in archive.namelist()
if member.count("/") == 1 and member.endswith(".dist-info/METADATA")
]
if len(members) != 1:
raise ValueError(f"{artifact.name}: expected one wheel METADATA file")
metadata = archive.read(members[0])
elif artifact.name.endswith(".tar.gz"):
name, version = parse_sdist_filename(artifact.name)
with tarfile.open(artifact) as archive:
members = [
member for member in archive.getmembers()
if member.isfile() and member.name.count("/") == 1
and member.name.endswith("/PKG-INFO")
]
if len(members) != 1:
raise ValueError(f"{artifact.name}: expected one sdist PKG-INFO file")
with archive.extractfile(members[0]) as stream:
metadata = stream.read()
else:
raise ValueError(f"Unexpected distribution: {artifact.name}")

if name != "pyalps" or version != expected:
raise ValueError(f"{artifact.name}: expected a pyalps {expected} distribution")
headers = BytesParser().parsebytes(metadata)
if (
canonicalize_name(headers.get("Name", "")) != "pyalps"
or Version(headers.get("Version", "")) != expected
):
raise ValueError(f"{artifact.name}: metadata does not describe pyalps {expected}")
print(f"Verified {artifact.name}")


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--ref", default=os.environ.get("GITHUB_REF", ""))
parser.add_argument("--dist", type=Path)
args = parser.parse_args()
try:
version = check_version(args.root, args.ref)
print(f"Release version: {version}")
if args.dist is not None:
check_distributions(args.dist, version)
except (OSError, ValueError, KeyError, tarfile.TarError, zipfile.BadZipFile) as error:
parser.exit(1, f"Release version check failed: {error}\n")


if __name__ == "__main__":
main()
Loading