Skip to content
Closed
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
106 changes: 106 additions & 0 deletions .github/scripts/check_version_bump.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""
Check version bump between two commits for pyproject.toml and src/quanteval/__init__.py.

Writes two outputs to the GitHub Actions runner via the GITHUB_OUTPUT file:
- should_release: 'true' or 'false'
- version: the new version string when should_release is 'true'

This script is intended to be invoked from a workflow step which sets
the environment variables `GITHUB_BEFORE` and `GITHUB_AFTER`.
"""
import os
import re
import subprocess
import sys


def read_at(sha, path):
# If sha is empty or all zeros, fallback to reading from the working tree
if not sha or set(sha) == {"0"}:
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except Exception:
return ""
try:
out = subprocess.check_output(["git", "show", f"{sha}:{path}"], stderr=subprocess.DEVNULL)
return out.decode("utf-8")
except subprocess.CalledProcessError:
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except Exception:
return ""


def extract_version_pyproject(text: str) -> str:
if not text:
return ""
m = re.search(r"^version\s*=\s*['\"]([^'\"]+)['\"]", text, re.M)
if m:
return m.group(1)
m = re.search(r"^\s*version\s*=\s*['\"]([^'\"]+)['\"]", text, re.M)
return m.group(1) if m else ""


def extract_version_init(text: str) -> str:
if not text:
return ""
m = re.search(r"__version__\s*=\s*['\"]([^'\"]+)['\"]", text)
return m.group(1) if m else ""


def write_output(pairs: dict):
# Write key=value pairs to a temp file so the workflow can load them.
out_file = os.environ.get("CHECK_OUTPUT_FILE", "/tmp/check_version_output.txt")
try:
with open(out_file, "a", encoding="utf-8") as f:
for k, v in pairs.items():
line = f"{k}={v}\n"
f.write(line)
# also print for logs
print(line.strip())
except Exception:
# fallback to stdout only
for k, v in pairs.items():
print(f"{k}={v}")


def main():
before = os.environ.get("GITHUB_BEFORE", "")
after = os.environ.get("GITHUB_AFTER", os.environ.get("GITHUB_SHA", ""))

py = "pyproject.toml"
init = "src/quanteval/__init__.py"

old_py = read_at(before, py)
new_py = read_at(after, py)
old_init = read_at(before, init)
new_init = read_at(after, init)

old_py_v = extract_version_pyproject(old_py)
new_py_v = extract_version_pyproject(new_py)
old_init_v = extract_version_init(old_init)
new_init_v = extract_version_init(new_init)

print("old_py_v=", old_py_v)
print("new_py_v=", new_py_v)
print("old_init_v=", old_init_v)
print("new_init_v=", new_init_v)

should_release = False
version = ""
if new_py_v and new_init_v and new_py_v == new_init_v:
if new_py_v != old_py_v and new_init_v != old_init_v:
should_release = True
version = new_py_v

if should_release:
write_output({"should_release": "true", "version": version})
else:
write_output({"should_release": "false"})


if __name__ == "__main__":
main()
61 changes: 56 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
name: Release

# Trigger when version files are modified or via manual dispatch
on:
push:
tags:
- 'v*'
# Only trigger when the two files that declare the package version are changed
paths:
- 'pyproject.toml'
- 'src/quanteval/__init__.py'
workflow_dispatch:
inputs:
dry_run:
Expand All @@ -14,41 +17,86 @@ on:
permissions:
contents: write
packages: write
# allow creating tags/releases
id-token: write

jobs:
release:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'
cache: pip

- name: Check version bump in pyproject.toml and __init__.py
id: check
env:
GITHUB_BEFORE: ${{ github.event.before }}
GITHUB_AFTER: ${{ github.sha }}
CHECK_OUTPUT_FILE: /tmp/check_version_output.txt
run: |
set -euo pipefail
# Run the check script which writes key=value pairs to $CHECK_OUTPUT_FILE
python .github/scripts/check_version_bump.py
# Show the file for logs
cat "$CHECK_OUTPUT_FILE" || true
# Append to GITHUB_OUTPUT so the step outputs are available
if [ -n "${GITHUB_OUTPUT-}" ] && [ -f "$CHECK_OUTPUT_FILE" ]; then
cat "$CHECK_OUTPUT_FILE" >> "$GITHUB_OUTPUT"
fi

- name: Stop if not a release commit
if: ${{ steps.check.outputs.should_release == 'false' }}
run: |
echo "No version bump detected or versions do not match; skipping release."
exit 0

- name: Create tag for release
if: ${{ steps.check.outputs.should_release == 'true' }}
env:
VERSION: ${{ steps.check.outputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
TAG=v${VERSION}
# avoid error if tag already exists
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag $TAG already exists, skipping tag creation."
else
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
fi

- name: Install build dependencies
if: ${{ steps.check.outputs.should_release == 'true' }}
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"

- name: Build release artifacts
if: ${{ steps.check.outputs.should_release == 'true' }}
run: python -m build

- name: Validate artifacts
if: ${{ steps.check.outputs.should_release == 'true' }}
run: twine check dist/*

- name: Publish to TestPyPI (dry-run)
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true' }}
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true' && steps.check.outputs.should_release == 'true' }}
run: |
python -m pip install --upgrade pip
python -m pip install twine
twine upload --repository-url https://test.pypi.org/legacy/ dist/* -u __token__ -p ${{ secrets.TEST_PYPI_TOKEN }} --skip-existing --verbose

- name: Publish to PyPI
if: ${{ !(github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true') }}
if: ${{ steps.check.outputs.should_release == 'true' && !(github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true') }}
uses: pypa/gh-action-pypi-publish@v1.5.0
with:
skip-existing: true
Expand All @@ -57,10 +105,13 @@ jobs:
password: ${{ secrets.PYPI_TOKEN }}

- name: Create GitHub release
if: ${{ steps.check.outputs.should_release == 'true' }}
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
tag_name: v${{ steps.check.outputs.version }}
name: Release v${{ steps.check.outputs.version }}
generate_release_notes: true
files: |
dist/*
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "quanteval"
version = "1.0.0"
version = "1.0.1"
description = "Quantitative strategy and factor evaluation toolkit for Chinese A-share and Hong Kong (HKEX) markets"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
2 changes: 1 addition & 1 deletion src/quanteval/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
License: MIT
"""

__version__ = '1.0.0'
__version__ = '1.0.1'

# Core components
from quanteval.core.strategy import Strategy
Expand Down
Loading