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
64 changes: 64 additions & 0 deletions .github/scripts/next_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Compute the next calendar version (YYYY.MAJOR.FIX) from git release tags.

Release tags look like ``v2026.1.0``. The first release of a calendar year is
``YYYY.1.0`` whatever the release type; after that a ``major`` release bumps
MAJOR and resets FIX, and a ``bugfix`` release bumps FIX.

Usage::

python .github/scripts/next_version.py {major,bugfix}

Prints the new version (without the leading ``v``). Exits non-zero if HEAD
already carries a release tag, so the same commit is not released twice.
"""
import argparse
import datetime
import re
import subprocess
import sys

TAG_PATTERN = re.compile(r"^v(\d{4})\.(\d+)\.(\d+)$")


def parse_tag(tag):
match = TAG_PATTERN.match(tag.strip())
if match is None:
return None
return tuple(int(part) for part in match.groups())


def next_version(tags, release_type, year):
releases = [v for v in (parse_tag(t) for t in tags) if v is not None]
latest = max(releases) if releases else None
if latest is None or latest[0] < year:
return year, 1, 0
if latest[0] > year:
raise ValueError("latest release v{}.{}.{} is dated after {}".format(*latest, year))
_, major, fix = latest
if release_type == "major":
return year, major + 1, 0
if release_type == "bugfix":
return year, major, fix + 1
raise ValueError("release type must be 'major' or 'bugfix', not {!r}".format(release_type))


def git_lines(*args):
output = subprocess.run(["git", *args], check=True, capture_output=True, text=True).stdout
return [line for line in output.splitlines() if line.strip()]


def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("release_type", choices=["major", "bugfix"])
args = parser.parse_args(argv)

head_releases = [t for t in git_lines("tag", "--points-at", "HEAD") if parse_tag(t)]
if head_releases:
sys.exit("HEAD is already released as {}".format(", ".join(head_releases)))

year = datetime.datetime.now(datetime.timezone.utc).year
print("{}.{}.{}".format(*next_version(git_lines("tag", "--list"), args.release_type, year)))


if __name__ == "__main__":
main()
56 changes: 54 additions & 2 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,49 @@
# Build and upload the package to PyPI. Run manually from the Actions tab.
# Release to PyPI. Run manually from the Actions tab on master and choose the release type.
# Versions are YYYY.MAJOR.FIX; the next one is computed from the v* release tags,
# and the tag plus a GitHub release are created only after the PyPI upload succeeds.
# The file name is registered as a PyPI trusted publisher: do not rename it.
name: Upload Package to PyPI

on:
workflow_dispatch:
inputs:
release_type:
description: "major (new features or changed results) or bugfix"
required: true
type: choice
options:
- bugfix
- major

concurrency:
group: pypi-release

jobs:
build:
name: Build distribution
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Compute next version
id: version
run: |
version=$(python3 .github/scripts/next_version.py "${{ inputs.release_type }}")
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Releasing $version" >> "$GITHUB_STEP_SUMMARY"
- name: Install pypa/build
run: python3 -m pip install build --user
- name: Build for upload
env:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ steps.version.outputs.version }}
run: python3 -m build --sdist
- name: Store the distribution packages
uses: actions/upload-artifact@v4
Expand All @@ -42,3 +69,28 @@ jobs:
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

tag-release:
name: Tag and create GitHub release
needs:
- build
- publish-to-pypi
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Create tag and release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.build.outputs.version }}
run: |
gh release create "v$VERSION" dist/* \
--target "$GITHUB_SHA" \
--title "mp_ephem $VERSION" \
--generate-notes
2 changes: 2 additions & 0 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jobs:

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python 3.9
uses: actions/setup-python@v3
with:
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ MANIFEST
venv
.DS_Store
.eggs

# written by setuptools_scm at build time
src/mp_ephem/__version__.py
*.egg-info
*.so
19 changes: 18 additions & 1 deletion README
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,24 @@ Started working on this on 10=APR=2012

INSTALL

python setup.py install
pip install mp_ephem (from PyPI)
pip install . (from a source checkout)

VERSIONS

Releases are numbered YYYY.MAJOR.FIX, e.g. 2026.2.1:

YYYY year of the release, so the age of a version is obvious
MAJOR increments for releases with new features or changed results
FIX increments for bug-fix releases

The first release of each year is YYYY.1.0. The version comes from the git
tag (v2026.2.1); nothing in the source needs editing. Builds between releases
get development versions such as 2026.2.2.dev3+g1a2b3c4.

To release, run the "Upload Package to PyPI" workflow on master from the
GitHub Actions tab and choose "major" or "bugfix". It computes the next
version, uploads to PyPI, then tags the commit and creates a GitHub release.

PYTHON USAGE:

Expand Down
12 changes: 12 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[build-system]
requires = ["setuptools>=64", "setuptools_scm>=8"]
build-backend = "setuptools.build_meta"

# Version is YYYY.MAJOR.FIX, taken from release tags like v2026.1.0.
# Untagged builds get a development version such as 2026.1.1.dev3+g1234abc.
[tool.setuptools_scm]
version_file = "src/mp_ephem/__version__.py"
tag_regex = '^v(?P<version>\d{4}\.\d+\.\d+)$'

[tool.setuptools_scm.scm.git]
describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "v[0-9][0-9][0-9][0-9].*"]
3 changes: 0 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@
import sys
from setuptools import setup, find_packages, Extension

version = "0.14.0"

dependencies = ['astropy', 'pyerfa', 'numpy', 'requests']

sources = [x for x in glob.glob('orbfit/*.c')]
headers = [x for x in glob.glob('orbfit/*.h')]

setup(name='mp_ephem',
version=version,
url='http://github.com/OSSOS/liborbfit',
author='''JJ Kavelaars (jjk@uvic.ca), Michele Bannister (micheleb@uvic.ca)''',
maintainer='M Bannister and JJ Kavelaars',
Expand Down
4 changes: 4 additions & 0 deletions src/mp_ephem/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
try:
from .__version__ import version as __version__
except ImportError: # source tree that has never been built
__version__ = "unknown"
from . import time_mpc
from .bk_orbit import BKOrbit
from .bk_orbit import BKOrbitError
Expand Down
1 change: 0 additions & 1 deletion src/mp_ephem/__version__.py

This file was deleted.

29 changes: 29 additions & 0 deletions test/test_next_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import importlib.util
import os
import unittest

_SCRIPT = os.path.join(os.path.dirname(__file__), os.pardir, '.github', 'scripts', 'next_version.py')
_spec = importlib.util.spec_from_file_location('next_version', _SCRIPT)
next_version = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(next_version)


class NextVersionTest(unittest.TestCase):

def test_first_release_ignores_legacy_tags(self):
self.assertEqual(next_version.next_version(['V12.0', '0.14.0'], 'major', 2026), (2026, 1, 0))
self.assertEqual(next_version.next_version([], 'bugfix', 2026), (2026, 1, 0))

def test_major_and_bugfix_within_a_year(self):
tags = ['v2026.1.0', 'v2026.2.0', 'v2026.2.3', 'v2026.10.0']
self.assertEqual(next_version.next_version(tags, 'major', 2026), (2026, 11, 0))
self.assertEqual(next_version.next_version(tags, 'bugfix', 2026), (2026, 10, 1))

def test_new_year_restarts_numbering(self):
tags = ['v2026.4.2']
self.assertEqual(next_version.next_version(tags, 'major', 2027), (2027, 1, 0))
self.assertEqual(next_version.next_version(tags, 'bugfix', 2027), (2027, 1, 0))

def test_rejects_release_dated_in_the_future(self):
with self.assertRaises(ValueError):
next_version.next_version(['v2027.1.0'], 'bugfix', 2026)
Loading