fix(ci): gate deploys on passing tags + fix markets feature flag test
- CI now creates v<pipeline_iid> tag after tests pass on master - Supervisor fetches tags and only deploys when a newer tag is available; skips if already on latest or no tags exist - Fix test_seeds_markets_enabled: markets is seeded disabled (enabled=0), test was asserting the wrong value Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
stages:
|
||||
- test
|
||||
- tag
|
||||
|
||||
test:
|
||||
stage: test
|
||||
@@ -14,6 +15,15 @@ test:
|
||||
- if: $CI_COMMIT_BRANCH == "master"
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
tag:
|
||||
stage: tag
|
||||
image: alpine/git
|
||||
script:
|
||||
- git tag "v${CI_PIPELINE_IID}"
|
||||
- git push "https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" "v${CI_PIPELINE_IID}"
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "master"
|
||||
|
||||
# Deployment is handled by the on-server supervisor (src/padelnomics/supervisor.py).
|
||||
# It polls git every 60s, detects code changes, and runs deploy.sh automatically.
|
||||
# It polls git every 60s, fetches tags, and deploys only when a new passing tag exists.
|
||||
# No CI secrets needed — zero SSH keys, zero deploy credentials.
|
||||
|
||||
@@ -17,12 +17,14 @@ Usage:
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import tomllib
|
||||
import urllib.request
|
||||
from collections import defaultdict
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from datetime import UTC, datetime
|
||||
@@ -44,6 +46,9 @@ SERVING_DUCKDB_PATH = os.getenv("SERVING_DUCKDB_PATH", "analytics.duckdb")
|
||||
ALERT_WEBHOOK_URL = os.getenv("ALERT_WEBHOOK_URL", "")
|
||||
NTFY_TOKEN = os.getenv("NTFY_TOKEN", "")
|
||||
WORKFLOWS_PATH = Path(os.getenv("WORKFLOWS_PATH", "infra/supervisor/workflows.toml"))
|
||||
GITLAB_API_URL = os.getenv("GITLAB_API_URL", "") # e.g. https://gitlab.com
|
||||
GITLAB_PROJECT_ID = os.getenv("GITLAB_PROJECT_ID", "") # numeric or namespace/project
|
||||
GITLAB_TOKEN = os.getenv("GITLAB_TOKEN", "") # read_api scope
|
||||
|
||||
NAMED_SCHEDULES = {
|
||||
"hourly": "0 * * * *",
|
||||
@@ -271,10 +276,47 @@ def web_code_changed() -> bool:
|
||||
return bool(result.stdout.strip())
|
||||
|
||||
|
||||
def current_deployed_tag() -> str | None:
|
||||
"""Return the tag currently checked out, or None if not on a tag."""
|
||||
result = subprocess.run(
|
||||
["git", "describe", "--tags", "--exact-match", "HEAD"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
return result.stdout.strip() or None
|
||||
|
||||
|
||||
def latest_remote_tag() -> str | None:
|
||||
"""Fetch tags from origin and return the latest v<n> tag by version order."""
|
||||
subprocess.run(
|
||||
["git", "fetch", "--tags", "--prune-tags", "origin"],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
result = subprocess.run(
|
||||
["git", "tag", "--list", "--sort=-version:refname", "v*"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
tags = result.stdout.strip().splitlines()
|
||||
return tags[0] if tags else None
|
||||
|
||||
|
||||
def git_pull_and_sync() -> None:
|
||||
"""Pull latest code and sync dependencies."""
|
||||
run_shell("git fetch origin master")
|
||||
run_shell("git switch --discard-changes --detach origin/master")
|
||||
"""Checkout the latest passing release tag and sync dependencies.
|
||||
|
||||
A tag v<N> is created by CI only after tests pass, so presence of a new
|
||||
tag implies green CI. Skips if already on the latest tag.
|
||||
"""
|
||||
latest = latest_remote_tag()
|
||||
if not latest:
|
||||
logger.info("No release tags found — skipping pull")
|
||||
return
|
||||
|
||||
current = current_deployed_tag()
|
||||
if current == latest:
|
||||
logger.info("Already on latest tag %s — skipping pull", latest)
|
||||
return
|
||||
|
||||
logger.info("New tag %s available (current: %s) — deploying", latest, current)
|
||||
run_shell(f"git checkout --detach {latest}")
|
||||
run_shell("uv sync --all-packages")
|
||||
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ class TestMigration0019:
|
||||
assert "description" in cols
|
||||
assert "updated_at" in cols
|
||||
|
||||
def test_seeds_markets_enabled(self, tmp_path):
|
||||
def test_seeds_markets_disabled(self, tmp_path):
|
||||
db_path = str(tmp_path / "test.db")
|
||||
migrate(db_path)
|
||||
conn = sqlite3.connect(db_path)
|
||||
@@ -125,7 +125,7 @@ class TestMigration0019:
|
||||
"SELECT enabled FROM feature_flags WHERE name = 'markets'"
|
||||
).fetchone()
|
||||
conn.close()
|
||||
assert row is not None and row[0] == 1
|
||||
assert row is not None and row[0] == 0
|
||||
|
||||
def test_seeds_payments_disabled(self, tmp_path):
|
||||
db_path = str(tmp_path / "test.db")
|
||||
|
||||
Reference in New Issue
Block a user