From 45a72b8d3b0a749eadf303ac5015010c04049902 Mon Sep 17 00:00:00 2001 From: whisper67265 Date: Tue, 26 May 2026 21:20:19 -0600 Subject: [PATCH 1/7] Add integration-smoke test flow --- .github/workflows/integration-smoke.yml | 32 +++++++ Makefile | 42 +++++++++ docker/Dockerfile.weblate-plugin | 28 ++++++ docker/README.md | 16 ++++ docker/docker-compose.yml | 60 ++++++++++++ pyproject.toml | 4 + scripts/README.md | 20 ++++ scripts/integration-smoke.sh | 43 +++++++++ scripts/lib/compose.sh | 21 +++++ scripts/lib/weblate-stack.sh | 61 ++++++++++++ tests/integration/__init__.py | 0 tests/integration/conftest.py | 56 +++++++++++ tests/integration/lib/__init__.py | 0 tests/integration/lib/docker_exec.py | 53 +++++++++++ tests/integration/lib/http.py | 59 ++++++++++++ tests/integration/test_smoke.py | 118 ++++++++++++++++++++++++ 16 files changed, 613 insertions(+) create mode 100644 .github/workflows/integration-smoke.yml create mode 100644 Makefile create mode 100644 docker/Dockerfile.weblate-plugin create mode 100644 docker/README.md create mode 100644 docker/docker-compose.yml create mode 100644 scripts/README.md create mode 100755 scripts/integration-smoke.sh create mode 100755 scripts/lib/compose.sh create mode 100755 scripts/lib/weblate-stack.sh create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/conftest.py create mode 100644 tests/integration/lib/__init__.py create mode 100644 tests/integration/lib/docker_exec.py create mode 100644 tests/integration/lib/http.py create mode 100644 tests/integration/test_smoke.py diff --git a/.github/workflows/integration-smoke.yml b/.github/workflows/integration-smoke.yml new file mode 100644 index 0000000..a5a5bf1 --- /dev/null +++ b/.github/workflows/integration-smoke.yml @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# +# SPDX-License-Identifier: BSL-1.0 + +name: Integration smoke + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + integration-smoke: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Run integration smoke tests + run: bash scripts/integration-smoke.sh + + - name: Upload logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: integration-smoke-logs + path: /tmp/compose-logs.txt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..70a226d --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# +# SPDX-License-Identifier: BSL-1.0 + +# Shared Makefile for CI scripts and CD deploys. +# Usage: make build && make up && make health + +COMPOSE_FILE ?= docker/docker-compose.yml +COMPOSE_PROJECT_NAME ?= cppa-weblate-plugin +COMPOSE = docker compose -f $(COMPOSE_FILE) -p $(COMPOSE_PROJECT_NAME) +WEBLATE_PORT ?= 8080 +HEALTH_TIMEOUT ?= 120 + +.PHONY: build up down logs health token + +build: + $(COMPOSE) build $(BUILD_ARGS) + +up: + $(COMPOSE) up -d + +down: + $(COMPOSE) down -v --remove-orphans + +logs: + $(COMPOSE) logs + +health: + @elapsed=0; \ + while [ $$elapsed -lt $(HEALTH_TIMEOUT) ]; do \ + if curl -sf http://localhost:$(WEBLATE_PORT)/healthz/ > /dev/null 2>&1; then \ + echo "Weblate healthy (after $${elapsed}s)"; exit 0; \ + fi; \ + sleep 5; \ + elapsed=$$((elapsed + 5)); \ + done; \ + echo "ERROR: Weblate not healthy after $(HEALTH_TIMEOUT)s"; \ + $(COMPOSE) logs weblate | tail -40; \ + exit 1 + +token: + @$(COMPOSE) exec -T weblate weblate createtoken admin | tail -1 diff --git a/docker/Dockerfile.weblate-plugin b/docker/Dockerfile.weblate-plugin new file mode 100644 index 0000000..db7f067 --- /dev/null +++ b/docker/Dockerfile.weblate-plugin @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# +# SPDX-License-Identifier: BSL-1.0 + +# Overlay image: stock Weblate + cppa-weblate-plugin installed into /app/venv. +# CI builds with repo root as context (installs checked-out branch). +# CD builds on the deploy server where the target branch is already checked out. + +FROM weblate/weblate:latest + +ARG PLUGIN_GIT_URL=https://github.com/cppalliance/cppa-weblate-plugin.git +ARG PLUGIN_GIT_REF= + +RUN /app/venv/bin/pip install --no-cache-dir uv + +COPY src/boost_weblate/settings_override.py /app/data/settings-override.py + +COPY . /tmp/plugin-src/ +RUN set -eux; \ + if [ ! -f /tmp/plugin-src/pyproject.toml ]; then \ + if [ -z "${PLUGIN_GIT_REF}" ]; then \ + echo "ERROR: No pyproject.toml in build context and PLUGIN_GIT_REF is unset"; exit 1; \ + fi; \ + rm -rf /tmp/plugin-src; \ + git clone --depth 1 --branch "${PLUGIN_GIT_REF}" "${PLUGIN_GIT_URL}" /tmp/plugin-src; \ + fi; \ + /app/venv/bin/uv pip install --python /app/venv/bin/python /tmp/plugin-src; \ + rm -rf /tmp/plugin-src diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..2b1c2fa --- /dev/null +++ b/docker/README.md @@ -0,0 +1,16 @@ +# docker/ + +Shared Docker assets for CI and CD. + +- **Dockerfile.weblate-plugin** — Overlay on `weblate/weblate:latest`; installs the plugin via `uv pip install` and copies `settings-override.py`. +- **docker-compose.yml** — PostgreSQL + Redis + Weblate stack. Override defaults via `.env` or environment variables. + +## Usage + +```bash +# From repo root: +docker compose -f docker/docker-compose.yml build +docker compose -f docker/docker-compose.yml up -d +``` + +Or use the Makefile: `make build && make up`. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..75bf811 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# +# SPDX-License-Identifier: BSL-1.0 + +# Shared Docker Compose stack for integration tests and CD deployments. +# CI: docker compose -f docker/docker-compose.yml build && up +# CD: same file, overridden via .env on the deploy server. + +services: + postgresql: + image: postgres:16-alpine + environment: + POSTGRES_USER: weblate + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-weblate} + POSTGRES_DB: weblate + healthcheck: + test: [CMD, pg_isready, -U, weblate] + interval: 5s + timeout: 3s + retries: 10 + tmpfs: + - /var/lib/postgresql/data + + redis: + image: redis:7-alpine + healthcheck: + test: [CMD, redis-cli, ping] + interval: 5s + timeout: 3s + retries: 10 + + weblate: + build: + context: .. + dockerfile: docker/Dockerfile.weblate-plugin + ports: + - ${WEBLATE_PORT:-8080}:8080 + depends_on: + postgresql: + condition: service_healthy + redis: + condition: service_healthy + environment: + WEBLATE_SITE_DOMAIN: ${WEBLATE_SITE_DOMAIN:-localhost:8080} + WEBLATE_ADMIN_PASSWORD: ${WEBLATE_ADMIN_PASSWORD:-admin} + WEBLATE_DEBUG: ${WEBLATE_DEBUG:-1} + POSTGRES_HOST: postgresql + POSTGRES_PORT: '5432' + POSTGRES_USER: weblate + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-weblate} + POSTGRES_DATABASE: weblate + REDIS_HOST: redis + REDIS_PORT: '6379' + CELERY_SINGLE_PROCESS: '1' + healthcheck: + test: [CMD, curl, -f, http://localhost:8080/healthz/] + interval: 10s + timeout: 5s + retries: 12 + start_period: 60s diff --git a/pyproject.toml b/pyproject.toml index cc0e9a3..74ad638 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,6 +125,10 @@ level = "cautious" unauthorized_licenses = [] [tool.pytest.ini_options] +addopts = ["-m", "not integration"] +markers = [ + "integration: requires live Weblate stack (Docker Compose) and optional WEBLATE_API_TOKEN" +] python_classes = ["Test*"] python_files = ["test_*.py", "*_test.py"] pythonpath = ["src", "."] diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..ada0231 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,20 @@ +# scripts/ + +Reusable shell scripts for CI and CD. + +- **lib/compose.sh** — Sets `COMPOSE_FILE`, `COMPOSE_PROJECT_NAME`, exports `compose()` wrapper. +- **lib/weblate-stack.sh** — Stack lifecycle functions: `stack_build`, `stack_up`, `stack_wait_healthy`, `stack_create_token`, `stack_logs`, `stack_down`. +- **integration-smoke.sh** — CI entrypoint for P0 smoke tests (build, start, health-check, test, teardown). + +## Usage + +```bash +# Run smoke tests locally: +bash scripts/integration-smoke.sh + +# Source the library for custom workflows: +source scripts/lib/weblate-stack.sh +stack_build +stack_up +stack_wait_healthy 120 +``` diff --git a/scripts/integration-smoke.sh b/scripts/integration-smoke.sh new file mode 100755 index 0000000..ca8f541 --- /dev/null +++ b/scripts/integration-smoke.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# SPDX-License-Identifier: BSL-1.0 + +# Integration smoke test entrypoint. +# Builds the stack, waits for health, creates a token, runs smoke tests. +# On exit (success or failure): collects logs and tears down the stack. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=lib/weblate-stack.sh +source "${SCRIPT_DIR}/lib/weblate-stack.sh" + +cleanup() { + local exit_code=$? + echo "--- Collecting logs ---" + stack_logs /tmp/compose-logs.txt + echo "--- Tearing down stack ---" + stack_down + exit "$exit_code" +} +trap cleanup EXIT + +echo "=== Building stack ===" +stack_build + +echo "=== Starting stack ===" +stack_up + +echo "=== Waiting for Weblate ===" +stack_wait_healthy "${HEALTH_TIMEOUT:-120}" + +echo "=== Creating API token ===" +WEBLATE_API_TOKEN="$(stack_create_token admin)" +export WEBLATE_API_TOKEN +export WEBLATE_LIVE_BASE_URL="${WEBLATE_LIVE_BASE_URL:-http://localhost:${WEBLATE_PORT:-8080}}" +export WEBLATE_COMPOSE_FILE="${COMPOSE_FILE}" +export WEBLATE_COMPOSE_PROJECT="${COMPOSE_PROJECT_NAME}" + +echo "=== Running smoke tests ===" +pip install --quiet pytest +pytest --override-ini addopts= tests/integration/test_smoke.py -v diff --git a/scripts/lib/compose.sh b/scripts/lib/compose.sh new file mode 100755 index 0000000..543c429 --- /dev/null +++ b/scripts/lib/compose.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# SPDX-License-Identifier: BSL-1.0 + +# Shared compose wrapper sourced by other scripts. +# Sets REPO_ROOT, COMPOSE_FILE, COMPOSE_PROJECT_NAME and exports compose(). + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +export REPO_ROOT + +COMPOSE_FILE="${COMPOSE_FILE:-${REPO_ROOT}/docker/docker-compose.yml}" +export COMPOSE_FILE + +COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-cppa-weblate-plugin}" +export COMPOSE_PROJECT_NAME + +compose() { + docker compose -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" "$@" +} diff --git a/scripts/lib/weblate-stack.sh b/scripts/lib/weblate-stack.sh new file mode 100755 index 0000000..c62acd6 --- /dev/null +++ b/scripts/lib/weblate-stack.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# SPDX-License-Identifier: BSL-1.0 + +# Reusable functions for managing the Weblate Docker Compose stack. +# Source this file from CI/CD scripts. + +set -euo pipefail + +SCRIPT_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=compose.sh +source "${SCRIPT_LIB_DIR}/compose.sh" + +stack_build() { + compose build "$@" +} + +stack_up() { + compose up -d "$@" +} + +stack_wait_healthy() { + local timeout="${1:-120}" + local port="${WEBLATE_PORT:-8080}" + local url="http://localhost:${port}/healthz/" + local interval=5 + local elapsed=0 + + echo "Waiting for Weblate at ${url} (timeout: ${timeout}s)..." + while [ "$elapsed" -lt "$timeout" ]; do + if curl -sf "$url" > /dev/null 2>&1; then + echo "Weblate is healthy (after ${elapsed}s)." + return 0 + fi + sleep "$interval" + elapsed=$((elapsed + interval)) + done + + echo "ERROR: Weblate did not become healthy in ${timeout}s." + echo "--- weblate container logs ---" + compose logs weblate | tail -80 + return 1 +} + +stack_create_token() { + local user="${1:-admin}" + compose exec -T weblate weblate createtoken "$user" | tail -1 +} + +stack_logs() { + local file="${1:-}" + if [ -n "$file" ]; then + compose logs > "$file" 2>&1 || true + else + compose logs + fi +} + +stack_down() { + compose down -v --remove-orphans 2>/dev/null || true +} diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 0000000..3d09914 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# +# SPDX-License-Identifier: BSL-1.0 + +"""Shared fixtures for integration tests.""" + +from __future__ import annotations + +import os +from collections.abc import Callable +from typing import Any + +import pytest + +from tests.integration.lib.docker_exec import ( + docker_exec_python, + docker_exec_python_json, +) +from tests.integration.lib.http import base_url as _base_url +from tests.integration.lib.http import http_get + + +@pytest.fixture(scope="session") +def api_token() -> str: + token = os.environ.get("WEBLATE_API_TOKEN") + if not token: + pytest.skip("WEBLATE_API_TOKEN not set") + return token + + +@pytest.fixture(scope="session") +def live_base_url() -> str: + return _base_url() + + +@pytest.fixture(scope="session") +def authed_get(api_token: str) -> Callable[..., tuple[int, Any]]: # noqa: E501 + """GET helper pre-bound with the API token.""" + token = api_token + + def _get(path: str, **kwargs: Any) -> tuple[int, Any]: + return http_get(path, token=token, **kwargs) + + return _get + + +@pytest.fixture(scope="session") +def exec_python() -> Callable[[str], str]: + """Execute a Python snippet inside the Weblate container.""" + return docker_exec_python + + +@pytest.fixture(scope="session") +def exec_python_json() -> Callable[[str], object]: + """Execute a Python snippet inside the container and parse JSON output.""" + return docker_exec_python_json diff --git a/tests/integration/lib/__init__.py b/tests/integration/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/lib/docker_exec.py b/tests/integration/lib/docker_exec.py new file mode 100644 index 0000000..a1b7bfc --- /dev/null +++ b/tests/integration/lib/docker_exec.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# +# SPDX-License-Identifier: BSL-1.0 + +"""Helper to execute Python snippets inside the running Weblate container.""" + +from __future__ import annotations + +import json +import os +import subprocess + + +def _compose_cmd() -> list[str]: + compose_file = os.environ.get( + "WEBLATE_COMPOSE_FILE", + "docker/docker-compose.yml", + ) + project = os.environ.get("WEBLATE_COMPOSE_PROJECT", "cppa-weblate-plugin") + return ["docker", "compose", "-f", compose_file, "-p", project] + + +def docker_exec_python(snippet: str, *, timeout: float = 30.0) -> str: + """Run a Python snippet inside the weblate container and return stdout.""" + cmd = [ + *_compose_cmd(), + "exec", + "-T", + "weblate", + "/app/venv/bin/python", + "-c", + snippet, + ] + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout, + check=False, + ) + if result.returncode != 0: + raise RuntimeError( + f"docker exec failed (rc={result.returncode}):\n" + f"stdout: {result.stdout}\n" + f"stderr: {result.stderr}" + ) + return result.stdout.strip() + + +def docker_exec_python_json(snippet: str, *, timeout: float = 30.0) -> object: + """Run a Python snippet that prints JSON and return the parsed result.""" + raw = docker_exec_python(snippet, timeout=timeout) + return json.loads(raw) diff --git a/tests/integration/lib/http.py b/tests/integration/lib/http.py new file mode 100644 index 0000000..93c3a3a --- /dev/null +++ b/tests/integration/lib/http.py @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# +# SPDX-License-Identifier: BSL-1.0 + +"""HTTP helper for integration tests — stdlib only (no requests/httpx).""" + +from __future__ import annotations + +import json +import os +import urllib.error +import urllib.request +from typing import Any + + +def base_url() -> str: + return os.environ.get("WEBLATE_LIVE_BASE_URL", "http://localhost:8080").rstrip("/") + + +def http_json( + method: str, + path: str, + *, + token: str | None = None, + body: dict[str, Any] | None = None, + timeout: float = 30.0, +) -> tuple[int, Any]: + """Perform an HTTP request and return ``(status_code, parsed_json_or_text)``.""" + url = f"{base_url()}{path}" + headers: dict[str, str] = {"Accept": "application/json"} + if token is not None: + headers["Authorization"] = f"Bearer {token}" + + data: bytes | None = None + if body is not None: + data = json.dumps(body).encode() + headers["Content-Type"] = "application/json" + + req = urllib.request.Request(url, data=data, headers=headers, method=method) + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + raw = resp.read() + code: int = resp.getcode() + except urllib.error.HTTPError as e: + raw = e.read() + code = e.code + + if not raw: + return code, None + try: + return code, json.loads(raw.decode()) + except (json.JSONDecodeError, UnicodeDecodeError): + return code, raw.decode(errors="replace") + + +def http_get( + path: str, *, token: str | None = None, timeout: float = 30.0 +) -> tuple[int, Any]: + return http_json("GET", path, token=token, timeout=timeout) diff --git a/tests/integration/test_smoke.py b/tests/integration/test_smoke.py new file mode 100644 index 0000000..2761785 --- /dev/null +++ b/tests/integration/test_smoke.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# +# SPDX-License-Identifier: BSL-1.0 + +"""P0 integration smoke tests. + +Verifies: +- Container boots with plugin installed (no import errors, no AppRegistryNotReady) +- WEBLATE_FORMATS contains QuickBookFormat +- INSTALLED_APPS contains boost_weblate.endpoint +- QuickBook format attributes (format_id, monolingual, autoload) +- Boost endpoint URL registration (plugin-ping, info with/without auth) +""" + +from __future__ import annotations + +from collections.abc import Callable + +import pytest + +from tests.integration.lib.http import http_get + +pytestmark = pytest.mark.integration + +# --------------------------------------------------------------------------- +# P0: Container boot + plugin load +# --------------------------------------------------------------------------- + + +class TestContainerBoot: + """Weblate container starts with plugin installed — no import errors.""" + + def test_weblate_healthz(self) -> None: + code, _ = http_get("/healthz/") + assert code == 200 + + def test_import_boost_weblate(self, exec_python: Callable[[str], str]) -> None: + output = exec_python("import boost_weblate; print('ok')") + assert output == "ok" + + def test_weblate_formats_contains_quickbook( + self, exec_python: Callable[[str], str] + ) -> None: + snippet = ( + "from django.conf import settings; " + "assert 'boost_weblate.formats.quickbook.QuickBookFormat' " + "in settings.WEBLATE_FORMATS, settings.WEBLATE_FORMATS" + ) + exec_python(snippet) + + def test_installed_apps_contains_endpoint( + self, exec_python: Callable[[str], str] + ) -> None: + snippet = ( + "from django.conf import settings; " + "apps = settings.INSTALLED_APPS; " + "assert any('boost_weblate.endpoint' in a for a in apps), apps" + ) + exec_python(snippet) + + +# --------------------------------------------------------------------------- +# P0: QuickBook format registration +# --------------------------------------------------------------------------- + + +class TestQuickBookFormat: + """QuickBook format registered with correct attributes.""" + + def test_quickbook_format_id(self, exec_python: Callable[[str], str]) -> None: + snippet = ( + "from boost_weblate.formats.quickbook import QuickBookFormat; " + "assert QuickBookFormat.format_id == 'quickbook', QuickBookFormat.format_id" + ) + exec_python(snippet) + + def test_quickbook_format_monolingual( + self, exec_python: Callable[[str], str] + ) -> None: + snippet = ( + "from boost_weblate.formats.quickbook import QuickBookFormat; " + "assert QuickBookFormat.monolingual is True, QuickBookFormat.monolingual" + ) + exec_python(snippet) + + def test_quickbook_format_autoload(self, exec_python: Callable[[str], str]) -> None: + snippet = ( + "from boost_weblate.formats.quickbook import QuickBookFormat; " + "assert QuickBookFormat.autoload == ('*.qbk',), QuickBookFormat.autoload" + ) + exec_python(snippet) + + +# --------------------------------------------------------------------------- +# P0: Boost endpoint URL registration +# --------------------------------------------------------------------------- + + +class TestBoostEndpointURLs: + """Boost endpoint routes are accessible via HTTP.""" + + def test_plugin_ping_no_auth(self) -> None: + code, body = http_get("/boost-endpoint/plugin-ping/") + assert code == 200 + assert body == "ok" or body == b"ok" + + def test_info_with_token(self, api_token: str) -> None: + code, body = http_get("/boost-endpoint/info/", token=api_token) + assert code == 200, f"unexpected {code}: {body}" + assert isinstance(body, dict) + assert body["module"] == "cppa-weblate-plugin" + assert "version" in body + assert "capabilities" in body + assert isinstance(body["capabilities"], list) + + def test_info_without_auth(self) -> None: + code, _ = http_get("/boost-endpoint/info/") + assert code in (401, 403) From 0457f9ad071d14b55a1552f5e295d7faf0b16468 Mon Sep 17 00:00:00 2001 From: whisper67265 Date: Tue, 26 May 2026 21:28:28 -0600 Subject: [PATCH 2/7] fix CI check-manifest fail --- Makefile | 42 ------------------------------------------ docker/README.md | 2 -- pyproject.toml | 2 ++ 3 files changed, 2 insertions(+), 44 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index 70a226d..0000000 --- a/Makefile +++ /dev/null @@ -1,42 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Andrew Zhang -# -# SPDX-License-Identifier: BSL-1.0 - -# Shared Makefile for CI scripts and CD deploys. -# Usage: make build && make up && make health - -COMPOSE_FILE ?= docker/docker-compose.yml -COMPOSE_PROJECT_NAME ?= cppa-weblate-plugin -COMPOSE = docker compose -f $(COMPOSE_FILE) -p $(COMPOSE_PROJECT_NAME) -WEBLATE_PORT ?= 8080 -HEALTH_TIMEOUT ?= 120 - -.PHONY: build up down logs health token - -build: - $(COMPOSE) build $(BUILD_ARGS) - -up: - $(COMPOSE) up -d - -down: - $(COMPOSE) down -v --remove-orphans - -logs: - $(COMPOSE) logs - -health: - @elapsed=0; \ - while [ $$elapsed -lt $(HEALTH_TIMEOUT) ]; do \ - if curl -sf http://localhost:$(WEBLATE_PORT)/healthz/ > /dev/null 2>&1; then \ - echo "Weblate healthy (after $${elapsed}s)"; exit 0; \ - fi; \ - sleep 5; \ - elapsed=$$((elapsed + 5)); \ - done; \ - echo "ERROR: Weblate not healthy after $(HEALTH_TIMEOUT)s"; \ - $(COMPOSE) logs weblate | tail -40; \ - exit 1 - -token: - @$(COMPOSE) exec -T weblate weblate createtoken admin | tail -1 diff --git a/docker/README.md b/docker/README.md index 2b1c2fa..39246eb 100644 --- a/docker/README.md +++ b/docker/README.md @@ -12,5 +12,3 @@ Shared Docker assets for CI and CD. docker compose -f docker/docker-compose.yml build docker compose -f docker/docker-compose.yml up -d ``` - -Or use the Makefile: `make build && make up`. diff --git a/pyproject.toml b/pyproject.toml index 74ad638..943c67e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,9 +145,11 @@ select = ["E", "F", "I", "UP"] module-name = "boost_weblate" module-root = "src" source-include = [ + "docker/**", "docs/**", "LICENSES/**", "REUSE.toml", + "scripts/**", "uv.lock", "tests/**" ] From f76667d68a82996d5878ba053d338bcd2091c240 Mon Sep 17 00:00:00 2001 From: whisper67265 Date: Tue, 26 May 2026 21:43:57 -0600 Subject: [PATCH 3/7] fix integration workflow fail --- docker/Dockerfile.weblate-plugin | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile.weblate-plugin b/docker/Dockerfile.weblate-plugin index db7f067..f15766f 100644 --- a/docker/Dockerfile.weblate-plugin +++ b/docker/Dockerfile.weblate-plugin @@ -8,10 +8,13 @@ FROM weblate/weblate:latest +# Base image ends with USER 1000; installing into /app/venv requires root. +USER root + ARG PLUGIN_GIT_URL=https://github.com/cppalliance/cppa-weblate-plugin.git ARG PLUGIN_GIT_REF= -RUN /app/venv/bin/pip install --no-cache-dir uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv COPY src/boost_weblate/settings_override.py /app/data/settings-override.py @@ -24,5 +27,7 @@ RUN set -eux; \ rm -rf /tmp/plugin-src; \ git clone --depth 1 --branch "${PLUGIN_GIT_REF}" "${PLUGIN_GIT_URL}" /tmp/plugin-src; \ fi; \ - /app/venv/bin/uv pip install --python /app/venv/bin/python /tmp/plugin-src; \ + uv pip install --python /app/venv/bin/python /tmp/plugin-src; \ rm -rf /tmp/plugin-src + +USER 1000 From 28e961b5934f1b5b4e19b5c313e90c8907f2c15a Mon Sep 17 00:00:00 2001 From: whisper67265 Date: Tue, 26 May 2026 21:50:38 -0600 Subject: [PATCH 4/7] fix token generation fail --- scripts/lib/weblate-stack.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/lib/weblate-stack.sh b/scripts/lib/weblate-stack.sh index c62acd6..3970986 100755 --- a/scripts/lib/weblate-stack.sh +++ b/scripts/lib/weblate-stack.sh @@ -44,7 +44,17 @@ stack_wait_healthy() { stack_create_token() { local user="${1:-admin}" - compose exec -T weblate weblate createtoken "$user" | tail -1 + # Weblate 2026+ removed `weblate createtoken`; issue a DRF token with Weblate's key shape (wlu_/wlp_). + compose exec -T -e "WEBLATE_CI_USERNAME=${user}" weblate \ + weblate shell -c \ + 'import os +from weblate.auth.models import User +from rest_framework.authtoken.models import Token +from weblate.utils.token import get_token +u = User.objects.get(username=os.environ["WEBLATE_CI_USERNAME"]) +Token.objects.filter(user=u).delete() +t = Token.objects.create(user=u, key=get_token("wlp" if u.is_bot else "wlu")) +print(t.key)' } stack_logs() { From fc38722bf58bf8f939e45ff53a57de4993ec7f9d Mon Sep 17 00:00:00 2001 From: whisper67265 Date: Tue, 26 May 2026 21:55:05 -0600 Subject: [PATCH 5/7] fix django load fail --- scripts/integration-smoke.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/integration-smoke.sh b/scripts/integration-smoke.sh index ca8f541..be25e0b 100755 --- a/scripts/integration-smoke.sh +++ b/scripts/integration-smoke.sh @@ -40,4 +40,5 @@ export WEBLATE_COMPOSE_PROJECT="${COMPOSE_PROJECT_NAME}" echo "=== Running smoke tests ===" pip install --quiet pytest -pytest --override-ini addopts= tests/integration/test_smoke.py -v +# Do not load tests/conftest.py (Django host setup); integration tests only need pytest + stdlib. +python -m pytest --confcutdir=tests/integration --override-ini addopts= tests/integration/test_smoke.py -v From 645af4c9f0e60c4158935baa89d713849746ed76 Mon Sep 17 00:00:00 2001 From: whisper67265 Date: Tue, 26 May 2026 22:00:47 -0600 Subject: [PATCH 6/7] fix smoke tests fail --- scripts/lib/weblate-stack.sh | 7 +++++-- tests/integration/lib/docker_exec.py | 13 ++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/scripts/lib/weblate-stack.sh b/scripts/lib/weblate-stack.sh index 3970986..caa7601 100755 --- a/scripts/lib/weblate-stack.sh +++ b/scripts/lib/weblate-stack.sh @@ -44,10 +44,13 @@ stack_wait_healthy() { stack_create_token() { local user="${1:-admin}" - # Weblate 2026+ removed `weblate createtoken`; issue a DRF token with Weblate's key shape (wlu_/wlp_). + # Use python -c (not `weblate shell`) so stdout is only the key compose exec -T -e "WEBLATE_CI_USERNAME=${user}" weblate \ - weblate shell -c \ + /app/venv/bin/python -c \ 'import os +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "weblate.settings_docker") +import django +django.setup() from weblate.auth.models import User from rest_framework.authtoken.models import Token from weblate.utils.token import get_token diff --git a/tests/integration/lib/docker_exec.py b/tests/integration/lib/docker_exec.py index a1b7bfc..4978f81 100644 --- a/tests/integration/lib/docker_exec.py +++ b/tests/integration/lib/docker_exec.py @@ -20,8 +20,19 @@ def _compose_cmd() -> list[str]: return ["docker", "compose", "-f", compose_file, "-p", project] +def _weblate_django_preamble() -> str: + """Weblate format modules need a configured Django app registry.""" + return ( + "import os; " + 'os.environ.setdefault("DJANGO_SETTINGS_MODULE", "weblate.settings_docker"); ' + "import django; " + "django.setup(); " + ) + + def docker_exec_python(snippet: str, *, timeout: float = 30.0) -> str: """Run a Python snippet inside the weblate container and return stdout.""" + code = _weblate_django_preamble() + snippet cmd = [ *_compose_cmd(), "exec", @@ -29,7 +40,7 @@ def docker_exec_python(snippet: str, *, timeout: float = 30.0) -> str: "weblate", "/app/venv/bin/python", "-c", - snippet, + code, ] result = subprocess.run( cmd, From 99af7ffbc30b269a1dc6cc8a0e2180f386df637f Mon Sep 17 00:00:00 2001 From: whisper67265 Date: Tue, 26 May 2026 22:14:17 -0600 Subject: [PATCH 7/7] fix coderabbitai review --- .github/workflows/integration-smoke.yml | 11 ++++++++--- scripts/integration-smoke.sh | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration-smoke.yml b/.github/workflows/integration-smoke.yml index a5a5bf1..c2860a9 100644 --- a/.github/workflows/integration-smoke.yml +++ b/.github/workflows/integration-smoke.yml @@ -15,9 +15,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 + # actions/checkout v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false - - uses: actions/setup-python@v5 + # actions/setup-python v6.2.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 with: python-version: '3.12' @@ -26,7 +30,8 @@ jobs: - name: Upload logs on failure if: failure() - uses: actions/upload-artifact@v4 + # actions/upload-artifact v4.6.2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: integration-smoke-logs path: /tmp/compose-logs.txt diff --git a/scripts/integration-smoke.sh b/scripts/integration-smoke.sh index be25e0b..d809e49 100755 --- a/scripts/integration-smoke.sh +++ b/scripts/integration-smoke.sh @@ -14,6 +14,7 @@ source "${SCRIPT_DIR}/lib/weblate-stack.sh" cleanup() { local exit_code=$? + set +e echo "--- Collecting logs ---" stack_logs /tmp/compose-logs.txt echo "--- Tearing down stack ---"