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
37 changes: 37 additions & 0 deletions .github/workflows/integration-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# 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:
# actions/checkout v6.0.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false

# actions/setup-python v6.2.0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
with:
python-version: '3.12'

- name: Run integration smoke tests
run: bash scripts/integration-smoke.sh

- name: Upload logs on failure
if: failure()
# actions/upload-artifact v4.6.2
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: integration-smoke-logs
path: /tmp/compose-logs.txt
33 changes: 33 additions & 0 deletions docker/Dockerfile.weblate-plugin
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# 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
Comment thread
whisper67265 marked this conversation as resolved.

# 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=

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

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; \
uv pip install --python /app/venv/bin/python /tmp/plugin-src; \
rm -rf /tmp/plugin-src

USER 1000
14 changes: 14 additions & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# 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
```
60 changes: 60 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# 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
Comment thread
whisper67265 marked this conversation as resolved.
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
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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", "."]
Expand All @@ -141,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/**"
]
20 changes: 20 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
@@ -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
```
45 changes: 45 additions & 0 deletions scripts/integration-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
# 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=$?
set +e
echo "--- Collecting logs ---"
stack_logs /tmp/compose-logs.txt
echo "--- Tearing down stack ---"
stack_down
exit "$exit_code"
Comment thread
whisper67265 marked this conversation as resolved.
}
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
# 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
21 changes: 21 additions & 0 deletions scripts/lib/compose.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
# 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" "$@"
}
74 changes: 74 additions & 0 deletions scripts/lib/weblate-stack.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
# 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}"
# Use python -c (not `weblate shell`) so stdout is only the key
compose exec -T -e "WEBLATE_CI_USERNAME=${user}" weblate \
/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
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() {
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
}
Empty file added tests/integration/__init__.py
Empty file.
56 changes: 56 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# 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
Empty file.
Loading
Loading