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
50 changes: 50 additions & 0 deletions .github/workflows/python-publish-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Upload dev Python Package

# Publish a dev pre-release to PyPI on tag push (from any branch), so a change can be
# validated on a canary environment before merging. Stable releases are NOT published this way:
# they go through a published GitHub Release (python-publish.yml).
#
# Usage: git tag v7.0.0-dev.1 && git push origin v7.0.0-dev.1
# Tags follow semantic versioning (vMAJOR.MINOR.PATCH-dev.N); the workflow translates to the
# PEP 440 equivalent (7.0.0.dev1) for the package. The version is derived from the tag (not
# from the VERSION file), and the workflow refuses anything that is not a vX.Y.Z-dev.N tag.
# pip only installs dev versions when pinned explicitly (or with --pre), so consumers can't
# pick one up by accident.

on:
push:
tags:
- "v*-dev.*"

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Refuse anything but a SemVer dev version tag
run: |
echo "${GITHUB_REF_NAME}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+-dev\.[0-9]+$' || {
echo "Tag ${GITHUB_REF_NAME} is not a vX.Y.Z-dev.N dev version, refusing to publish."
exit 1
}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Set package version from the tag (SemVer -> PEP 440)
run: |
SEMVER="${GITHUB_REF_NAME#v}"
echo "${SEMVER/-dev./.dev}" > VERSION
echo "Publishing version $(cat VERSION) from tag ${GITHUB_REF_NAME}"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
# Changelog

## Unreleased
## v7.0.0 (2026-07-08)

### BREAKING CHANGE
- Raise minimum supported Python version to 3.13.

### BREAKING CHANGE
- Drop the `wiremind-kubernetes` dependency (library being decommissioned): the subset
chartreuse uses (`KubernetesDeploymentManager` with `stop_pods`/`start_pods`, kube config
loading, `run_command`) now lives in `chartreuse.kubernetes_helper` and
`chartreuse.utils.command`, depending only on the official `kubernetes` client.
Library users importing chartreuse should not be affected unless they relied on chartreuse
pulling in wiremind-kubernetes transitively.

### Feat
- New `chartreuse-restore` entrypoint, to run as a post-deployment (e.g. ArgoCD PostSync) hook
when `CHARTREUSE_UPGRADE_BEFORE_DEPLOYMENT` is enabled. On that path `chartreuse-upgrade`
skips `start_pods()` and relies on the following deployment to restore replicas, which never
happens for HPA-managed Deployments (their chart omits `spec.replicas`, and an HPA whose
target is at 0 replicas with `minReplicas >= 1` is ScalingDisabled): they stayed stranded at
0 replicas after every migration. `stop_pods()` now marks every Deployment it stops with the
`wiremind.io/stopped-by` annotation; the restore only touches Deployments still carrying it.

### Chore
- Validate runtime/tooling compatibility on Python 3.14.
- CI: pushing a SemVer `vX.Y.Z-dev.N` tag (from any branch) publishes the PEP 440 dev
pre-release `X.Y.Z.devN` to PyPI, for validating changes on a canary environment before
merging. Stable releases still require a published GitHub Release.

## v6.0.0 (2025-10-07)

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6.2.1
7.0.0
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ readme = "README.md"
authors = [{ name = "wiremind", email = "dev@wiremind.io" }]
license="LGPL-3.0-or-later"
urls = { github = "https://github.com/wiremind/chartreuse"}
scripts = {chartreuse-upgrade = "chartreuse.chartreuse_upgrade:main"}
scripts = {chartreuse-upgrade = "chartreuse.chartreuse_upgrade:main", chartreuse-restore = "chartreuse.chartreuse_restore:main"}
requires-python = ">=3.13.0"
classifiers = [
"Programming Language :: Python :: 3",
Expand All @@ -16,10 +16,10 @@ classifiers = [

dependencies = [
"alembic",
"kubernetes",
"psycopg2",
"pydantic>=2.0.0",
"PyYAML",
"wiremind-kubernetes~=7.0",
]

[build-system]
Expand Down
6 changes: 1 addition & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@ durationpy==0.10
# via kubernetes
google-auth==2.41.1
# via kubernetes
greenlet==3.2.4
# via sqlalchemy
idna==3.10
# via requests
kubernetes==34.1.0
# via wiremind-kubernetes
# via chartreuse (pyproject.toml)
mako==1.3.10
# via alembic
markupsafe==3.0.3
Expand Down Expand Up @@ -75,5 +73,3 @@ urllib3==2.3.0
# requests
websocket-client==1.9.0
# via kubernetes
wiremind-kubernetes==7.5.0
# via chartreuse (pyproject.toml)
9 changes: 3 additions & 6 deletions src/chartreuse/chartreuse.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import logging

import wiremind_kubernetes.kubernetes_helper

from .config_loader import DatabaseConfig
from .kubernetes_helper import KubernetesDeploymentManager
from .utils import AlembicMigrationHelper

logger = logging.getLogger(__name__)
Expand All @@ -23,7 +22,7 @@ def __init__(
self,
databases_config: dict[str, DatabaseConfig],
release_name: str,
kubernetes_helper: wiremind_kubernetes.kubernetes_helper.KubernetesDeploymentManager | None = None,
kubernetes_helper: KubernetesDeploymentManager | None = None,
):
configure_logging()

Expand Down Expand Up @@ -56,9 +55,7 @@ def __init__(
if kubernetes_helper:
self.kubernetes_helper = kubernetes_helper
else:
self.kubernetes_helper = wiremind_kubernetes.kubernetes_helper.KubernetesDeploymentManager(
use_kubeconfig=None, release_name=release_name
)
self.kubernetes_helper = KubernetesDeploymentManager(use_kubeconfig=None, release_name=release_name)

@property
def is_migration_needed(self) -> bool:
Expand Down
36 changes: 36 additions & 0 deletions src/chartreuse/chartreuse_restore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import logging
import os

from .chartreuse import configure_logging
from .chartreuse_upgrade import ensure_safe_run
from .kubernetes_helper import KubernetesDeploymentManager

logger = logging.getLogger(__name__)


def main() -> None:
"""
Restore Deployments that a pre-deployment migration (CHARTREUSE_UPGRADE_BEFORE_DEPLOYMENT)
stopped and that the deployment itself could not scale back up.

On that path chartreuse-upgrade intentionally skips start_pods() and relies on the
deployment that follows to restore replicas. That works for Deployments whose chart sets
spec.replicas, but HPA-managed ones omit it: they stay at 0 replicas and an HPA whose
target is at 0 with minReplicas >= 1 is ScalingDisabled and never scales them back up.

Runs as a post-deployment (e.g. ArgoCD PostSync) hook: the new pod template is already
applied, so scaling up only ever starts new-version pods. Only Deployments still carrying
the annotation set by stop_pods() are touched. If this program fails, the deployment is
considered as failed: stranded workers must be visible, not a log line.
"""
configure_logging()
ensure_safe_run()

release_name: str = os.environ["CHARTREUSE_RELEASE_NAME"]

deployment_manager = KubernetesDeploymentManager(release_name=release_name, use_kubeconfig=None)
deployment_manager.restore_stopped_pods()


if __name__ == "__main__":
main()
3 changes: 1 addition & 2 deletions src/chartreuse/chartreuse_upgrade.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import logging
import os

from wiremind_kubernetes import KubernetesDeploymentManager

from chartreuse import get_version

from .chartreuse import Chartreuse
from .config_loader import load_multi_database_config
from .kubernetes_helper import KubernetesDeploymentManager

logger = logging.getLogger(__name__)

Expand Down
Loading
Loading