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
100 changes: 100 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# SPDX-FileCopyrightText: 2026 bartzbeielstein
# SPDX-License-Identifier: AGPL-3.0-or-later

name: Release

on:
push:
branches:
- main
- develop

# Principle of Least Privilege: Base permissions are read-only
permissions: read-all

jobs:
release:
name: Create Release
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
if: "!contains(github.event.head_commit.message, 'chore(release)')"

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.13"

- name: Set up uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86

- name: Install dependencies
run: |
uv pip install --system build twine
uv pip install --system -e ".[dev]"

- name: Run tests
run: |
pytest tests/ -v

- name: Verify package name in pyproject.toml
run: |
grep -q 'name = "spotanomaly2"' pyproject.toml || (echo "ERROR: Package name mismatch in pyproject.toml" && exit 1)
echo "Package name verified: spotanomaly2"

- name: Build distribution packages
run: |
python -m build
echo "Build artifacts created:"
ls -lah dist/

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20

- name: Install semantic-release
run: |
npm install -g semantic-release@23 \
@semantic-release/git@10 \
@semantic-release/changelog@6 \
@semantic-release/exec@6 \
conventional-changelog-conventionalcommits@7

- name: Semantic Release
env:
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }}
run: npx semantic-release

- name: Publish to PyPI
if: success()
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
with:
packages-dir: dist/
skip-existing: true

- name: Verify PyPI Release
if: success()
run: |
echo "Release completed successfully!"
echo "Verifying package on PyPI..."
for i in {1..30}; do
if python -c "import urllib.request; urllib.request.urlopen('https://pypi.org/project/spotanomaly2/')" 2>/dev/null; then
echo "Package found on PyPI"
break
fi
if [ $i -lt 30 ]; then
echo "Waiting for PyPI to sync (attempt $i/30)..."
sleep 10
fi
done
171 changes: 171 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
{
"branches": [
{
"name": "main",
"prerelease": false
},
{
"name": "develop",
"prerelease": "rc"
}
],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": [
{
"type": "feat",
"release": "minor"
},
{
"type": "fix",
"release": "patch"
},
{
"type": "perf",
"release": "patch"
},
{
"type": "revert",
"release": "patch"
},
{
"type": "docs",
"release": false
},
{
"type": "style",
"release": false
},
{
"type": "chore",
"release": false
},
{
"type": "refactor",
"release": "patch"
},
{
"type": "test",
"release": false
},
{
"type": "build",
"release": false
},
{
"type": "ci",
"release": false
},
{
"breaking": true,
"release": "major"
}
]
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits",
"presetConfig": {
"types": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
},
{
"type": "perf",
"section": "Performance Improvements"
},
{
"type": "revert",
"section": "Reverts"
},
{
"type": "docs",
"section": "Documentation",
"hidden": false
},
{
"type": "style",
"section": "Styles",
"hidden": true
},
{
"type": "chore",
"section": "Miscellaneous Chores",
"hidden": true
},
{
"type": "refactor",
"section": "Code Refactoring"
},
{
"type": "test",
"section": "Tests",
"hidden": true
},
{
"type": "build",
"section": "Build System",
"hidden": true
},
{
"type": "ci",
"section": "Continuous Integration",
"hidden": true
}
]
}
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
[
"@semantic-release/exec",
{
"prepareCmd": "sed -i 's/^version = .*/version = \"${nextRelease.version}\"/' pyproject.toml"
}
],
[
"@semantic-release/exec",
{
"prepareCmd": "rm -rf dist && python -m build"
}
],
[
"@semantic-release/github",
{
"assets": [
{
"path": "dist/*.whl"
},
{
"path": "dist/*.tar.gz"
}
]
}
],
[
"@semantic-release/git",
{
"assets": [
"CHANGELOG.md",
"pyproject.toml"
],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}
7 changes: 7 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ precedence = "aggregate"
SPDX-FileCopyrightText = "2026 bartzbeielstein"
SPDX-License-Identifier = "AGPL-3.0-or-later"

[[annotations]]
path = [".releaserc.json", "CHANGELOG.md"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2026 bartzbeielstein"
SPDX-License-Identifier = "AGPL-3.0-or-later"
SPDX-FileComment = "semantic-release config and its generated changelog (no inline-comment support)."

[[annotations]]
path = "PREPROCESSING_ISSUES.md"
precedence = "aggregate"
Expand Down
25 changes: 11 additions & 14 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ dependencies = [
"joblib>=1.3.0",
"typing_extensions>=4.5.0",
"watchdog>=3.0.0",
"spotanomaly2_safe",
"spotforecast2",
"spotforecast2-safe",
"spotanomaly2-safe>=0.1.0",
"spotforecast2>=3.5.0",
"spotforecast2-safe>=15.6.0",
"spotoptim>=0.0.160",
"catboost>=1.2.10",
"scipy-stubs~=1.17.1",
Expand Down Expand Up @@ -73,9 +73,6 @@ packages = ["spotanomaly2"]
[tool.hatch.build.targets.wheel.force-include]
"assets" = "assets"

[tool.hatch.metadata]
allow-direct-references = true

[tool.ruff]
line-length = 120
target-version = "py310"
Expand Down Expand Up @@ -104,14 +101,14 @@ target-version = ["py310", "py311", "py312"]
[lint.pydocstyle]
convention = 'google'

[tool.uv.sources]
spotanomaly2_safe = { git = "https://github.com/sequential-parameter-optimization/spotanomaly2-safe.git" }
spotforecast2 = { git = "https://github.com/sequential-parameter-optimization/spotforecast2.git" }
spotforecast2-safe = { git = "https://github.com/sequential-parameter-optimization/spotforecast2-safe.git" }
# For local development, uncomment the lines below and comment out the git sources above:
# spotanomaly2_safe = { path = "../spotanomaly2_safe", editable = true }
# spotforecast2 = { path = "../spotforecast2", editable = true }
# spotforecast2-safe = { path = "../spotforecast2-safe", editable = true }
# NOTE: The three first-party deps (spotanomaly2-safe, spotforecast2,
# spotforecast2-safe) are now consumed as normal versioned PyPI requirements so
# the built wheel is installable via `pip install spotanomaly2`. For local
# development against checked-out sibling repos, add a *local* (git-ignored)
# override instead of editing this file — e.g. create `uv.toml` with:
# [sources]
# spotanomaly2-safe = { path = "../spotanomaly2-safe", editable = true }
# or run `uv pip install -e ../spotanomaly2-safe`.

[dependency-groups]
dev = [
Expand Down
2 changes: 1 addition & 1 deletion spotanomaly2/domain/spotforecast_adapter/tuner.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def tune_panel(
best_model, model_scores}``. ``model_scores`` is a sub-dict per
candidate model so callers can compare them, not just the winner.
"""
from spotforecast2.model_selection import OneStepAheadFold
from spotforecast2_safe.splitter import OneStepAheadFold
from tqdm.auto import tqdm

train_settings = resolve_train_settings(self.config)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_spotforecast_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ def test_tune_uses_full_data_no_outer_holdout(self, sample_config):
config["train"]["lags"] = 6
tuner = SpotforecastTuner(config)
# Capture the OneStepAheadFold that the tuner actually builds.
from spotforecast2.model_selection import OneStepAheadFold
from spotforecast2_safe.splitter import OneStepAheadFold

captured: dict = {}
real_init = OneStepAheadFold.__init__
Expand Down Expand Up @@ -432,9 +432,9 @@ def test_tuner_r2_matches_production_r2_under_differentiation(self, sample_confi
from sklearn.linear_model import Ridge
from sklearn.metrics import r2_score
from sklearn.preprocessing import StandardScaler
from spotforecast2.model_selection import OneStepAheadFold
from spotforecast2_safe.forecaster.recursive import ForecasterRecursive
from spotforecast2_safe.forecaster.utils import transform_numpy
from spotforecast2_safe.splitter import OneStepAheadFold

from spotanomaly2.domain.spotforecast_adapter import (
_build_raw_r2_under_differentiation,
Expand Down
Loading
Loading