merge: ci gate deploys on passing tags + fix markets feature flag test
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
|
- tag
|
||||||
|
|
||||||
test:
|
test:
|
||||||
stage: test
|
stage: test
|
||||||
@@ -14,6 +15,15 @@ test:
|
|||||||
- if: $CI_COMMIT_BRANCH == "master"
|
- if: $CI_COMMIT_BRANCH == "master"
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
- 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).
|
# 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.
|
# No CI secrets needed — zero SSH keys, zero deploy credentials.
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ Usage:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import tomllib
|
import tomllib
|
||||||
|
import urllib.request
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from datetime import UTC, datetime
|
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", "")
|
ALERT_WEBHOOK_URL = os.getenv("ALERT_WEBHOOK_URL", "")
|
||||||
NTFY_TOKEN = os.getenv("NTFY_TOKEN", "")
|
NTFY_TOKEN = os.getenv("NTFY_TOKEN", "")
|
||||||
WORKFLOWS_PATH = Path(os.getenv("WORKFLOWS_PATH", "infra/supervisor/workflows.toml"))
|
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 = {
|
NAMED_SCHEDULES = {
|
||||||
"hourly": "0 * * * *",
|
"hourly": "0 * * * *",
|
||||||
@@ -271,10 +276,47 @@ def web_code_changed() -> bool:
|
|||||||
return bool(result.stdout.strip())
|
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:
|
def git_pull_and_sync() -> None:
|
||||||
"""Pull latest code and sync dependencies."""
|
"""Checkout the latest passing release tag and sync dependencies.
|
||||||
run_shell("git fetch origin master")
|
|
||||||
run_shell("git switch --discard-changes --detach origin/master")
|
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")
|
run_shell("uv sync --all-packages")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class TestMigration0019:
|
|||||||
assert "description" in cols
|
assert "description" in cols
|
||||||
assert "updated_at" 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")
|
db_path = str(tmp_path / "test.db")
|
||||||
migrate(db_path)
|
migrate(db_path)
|
||||||
conn = sqlite3.connect(db_path)
|
conn = sqlite3.connect(db_path)
|
||||||
@@ -125,7 +125,7 @@ class TestMigration0019:
|
|||||||
"SELECT enabled FROM feature_flags WHERE name = 'markets'"
|
"SELECT enabled FROM feature_flags WHERE name = 'markets'"
|
||||||
).fetchone()
|
).fetchone()
|
||||||
conn.close()
|
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):
|
def test_seeds_payments_disabled(self, tmp_path):
|
||||||
db_path = str(tmp_path / "test.db")
|
db_path = str(tmp_path / "test.db")
|
||||||
|
|||||||
Reference in New Issue
Block a user