Skip to content
Open
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
8 changes: 4 additions & 4 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ GitHub Actions and CI/CD helpers for this repository.
| [`workflows/ci-test.yml`](workflows/ci-test.yml) | Unit tests and coverage |
| [`workflows/ci-package.yml`](workflows/ci-package.yml) | Build and package checks |
| [`workflows/ci-dependencies.yml`](workflows/ci-dependencies.yml) | Dependency and license audit |
| [`workflows/ci-combination-smoke.yml`](workflows/ci-combination-smoke.yml) | Integration smoke (Docker stack) |
| [`workflows/ci-combination-functional.yml`](workflows/ci-combination-functional.yml) | Integration functional tests |
| [`workflows/ci-combination-auth.yml`](workflows/ci-combination-auth.yml) | Integration auth tests |
| [`workflows/ci-plugin-smoke.yml`](workflows/ci-plugin-smoke.yml) | Plugin smoke (Docker stack) |
| [`workflows/ci-plugin-functional.yml`](workflows/ci-plugin-functional.yml) | Plugin functional tests |
| [`workflows/ci-plugin-auth.yml`](workflows/ci-plugin-auth.yml) | Plugin auth tests |

Callable workflows (`ci-*`, `ci-combination-*`) are triggered only via `workflow_call` from `ci.yml`, not directly on push.
Callable workflows (`ci-*`, `ci-plugin-*`) are triggered only via `workflow_call` from `ci.yml`, not directly on push.

## Other paths

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: BSL-1.0

name: Combination auth
name: Plugin auth

on:
workflow_call:
Expand All @@ -12,8 +12,8 @@ permissions:
contents: read

jobs:
combination-auth:
name: Combination auth
plugin-auth:
name: Plugin auth
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand All @@ -32,13 +32,13 @@ jobs:
with:
version: 0.11.12

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

- name: Upload logs on failure
if: failure()
# actions/upload-artifact v4.6.2
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ci-combination-auth-logs
name: ci-plugin-auth-logs
path: /tmp/compose-logs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
#
# SPDX-License-Identifier: BSL-1.0

name: Combination functional
name: Plugin functional

on:
workflow_call:
secrets:
# GitHub classic PAT with `repo` scope (repository secret, not a variable).
# Used by tests/integration/lib/gh_repo.py to create a temporary repo,
# Used by tests/plugin/lib/gh_repo.py to create a temporary repo,
# push QuickBook fixtures, register Weblate's SSH deploy key, and delete
# the repo after the run. Enables add-or-update / BoostComponentService E2E
# in tests/integration/test_functional.py. If unset, those tests are skipped
# in tests/plugin/test_functional.py. If unset, those tests are skipped
# (smoke round-trip and other non-GitHub tests still run). Inherited from
# the caller via secrets: inherit in ci.yml.
GH_TEST_REPO_TOKEN:
Expand All @@ -22,8 +22,8 @@ permissions:
contents: read

jobs:
combination-functional:
name: Combination functional
plugin-functional:
name: Plugin functional
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
Expand All @@ -42,15 +42,15 @@ jobs:
with:
version: 0.11.12

- name: Run integration functional tests
- name: Run plugin functional tests
env:
GH_TEST_REPO_TOKEN: ${{ secrets.GH_TEST_REPO_TOKEN }}
run: bash scripts/integration-functional.sh
run: bash scripts/plugin-functional.sh

- name: Upload logs on failure
if: failure()
# actions/upload-artifact v4.6.2
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ci-combination-functional-logs
name: ci-plugin-functional-logs
path: /tmp/compose-logs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: BSL-1.0

name: Combination smoke
name: Plugin smoke

on:
workflow_call:
Expand All @@ -12,8 +12,8 @@ permissions:
contents: read

jobs:
combination-smoke:
name: Combination smoke
plugin-smoke:
name: Plugin smoke
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
Expand All @@ -32,13 +32,13 @@ jobs:
with:
version: 0.11.12

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

- name: Upload logs on failure
if: failure()
# actions/upload-artifact v4.6.2
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ci-combination-smoke-logs
name: ci-plugin-smoke-logs
path: /tmp/compose-logs.txt
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ jobs:
uses: ./.github/workflows/ci-package.yml
dependencies:
uses: ./.github/workflows/ci-dependencies.yml
combination-smoke:
uses: ./.github/workflows/ci-combination-smoke.yml
combination-functional:
uses: ./.github/workflows/ci-combination-functional.yml
plugin-smoke:
uses: ./.github/workflows/ci-plugin-smoke.yml
plugin-functional:
uses: ./.github/workflows/ci-plugin-functional.yml
secrets: inherit
combination-auth:
uses: ./.github/workflows/ci-combination-auth.yml
plugin-auth:
uses: ./.github/workflows/ci-plugin-auth.yml
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ flowchart TB

- **`src/boost_weblate/endpoint/`** — **HTTP API** for Boost documentation project/component management. Exposes three routes under `/boost-endpoint/` (see [Boost endpoint routes](#boost-endpoint-routes)), uses Django REST Framework for auth and serialization, and hands off heavy work to a Celery task (see [Celery requirement for add-or-update](#celery-requirement-for-add-or-update)).

- **`tests/`** — **Pytest** layout mirrors `src/boost_weblate/` (`tests/formats/`, `tests/utils/`, `tests/endpoint/`). Shared fixtures live under `tests/fixtures/`. `tests/conftest.py` configures `sys.path`, sets `DJANGO_SETTINGS_MODULE` to `tests.django_qbk_format_settings`, and calls `django.setup()` so format tests can load Weblate's Django stack without requiring PostgreSQL. Docker-based integration tests live in `tests/integration/`.
- **`tests/`** — **Pytest** layout mirrors `src/boost_weblate/` (`tests/formats/`, `tests/utils/`, `tests/endpoint/`). Shared fixtures live under `tests/fixtures/`. `tests/conftest.py` configures `sys.path`, sets `DJANGO_SETTINGS_MODULE` to `tests.django_qbk_format_settings`, and calls `django.setup()` so format tests can load Weblate's Django stack without requiring PostgreSQL. Docker-based plugin tests live in `tests/plugin/`.

## WEBLATE_FORMATS configuration

Expand Down Expand Up @@ -265,31 +265,31 @@ Triggered on push and PR to `main` and `develop`. Calls seven reusable sub-workf
| `test` | [`.github/workflows/ci-test.yml`](.github/workflows/ci-test.yml) | pytest + 90% coverage gate (`--cov-fail-under=90`) |
| `package` | [`.github/workflows/ci-package.yml`](.github/workflows/ci-package.yml) | `uv build`, twine, pydistcheck, pyroma, check-wheel-contents, check-manifest |
| `dependencies` | [`.github/workflows/ci-dependencies.yml`](.github/workflows/ci-dependencies.yml) | pip-audit, liccheck, dependency review (on PRs) |
| `combination-smoke` | [`.github/workflows/ci-combination-smoke.yml`](.github/workflows/ci-combination-smoke.yml) | Docker stack → P0 smoke tests (`scripts/integration-smoke.sh`) |
| `combination-auth` | [`.github/workflows/ci-combination-auth.yml`](.github/workflows/ci-combination-auth.yml) | Docker stack → auth tests (`scripts/integration-auth.sh`) |
| `combination-functional` | [`.github/workflows/ci-combination-functional.yml`](.github/workflows/ci-combination-functional.yml) | Docker stack → E2E functional tests (`scripts/integration-functional.sh`); optional `GH_TEST_REPO_TOKEN` secret for GitHub-backed tests |
| `plugin-smoke` | [`.github/workflows/ci-plugin-smoke.yml`](.github/workflows/ci-plugin-smoke.yml) | Docker stack → P0 smoke tests (`scripts/plugin-smoke.sh`) |
| `plugin-auth` | [`.github/workflows/ci-plugin-auth.yml`](.github/workflows/ci-plugin-auth.yml) | Docker stack → auth tests (`scripts/plugin-auth.sh`) |
| `plugin-functional` | [`.github/workflows/ci-plugin-functional.yml`](.github/workflows/ci-plugin-functional.yml) | Docker stack → E2E functional tests (`scripts/plugin-functional.sh`); optional `GH_TEST_REPO_TOKEN` secret for GitHub-backed tests |

All `ci-combination-*` jobs build the CI Docker stack (`docker/docker-compose.ci.yml`), wait for the healthcheck, create an API token, run the corresponding pytest suite under `tests/integration/`, and tear down.
All `ci-plugin-*` jobs build the CI Docker stack (`docker/docker-compose.ci.yml`), wait for the healthcheck, create an API token, run the corresponding pytest suite under `tests/plugin/`, and tear down.

### CD (`cd.yml`)

Triggered after CI succeeds on a `develop` push. SSHes into the staging server at `/opt/cppa-weblate-plugin`, pulls the latest code, rebuilds the CD Docker image (`docker/docker-compose.cd.yml`), brings the stack up, and polls `${WEBLATE_URL_PREFIX}/healthz/` on `WEBLATE_PORT` (from `.env`) for up to 180 s. On failure, logs the last 40 lines and exits non-zero. Concurrency is locked per branch so deploys never overlap.

Full deployment procedure: [docs/deployment-runbook.md](docs/deployment-runbook.md).

### Running integration tests locally
### Running plugin tests locally

```bash
# Smoke (P0 — container boot, format registration, URL registration):
bash scripts/integration-smoke.sh
bash scripts/plugin-smoke.sh

# Auth (token auth on protected routes; ping stays public):
bash scripts/integration-auth.sh
bash scripts/plugin-auth.sh

# Functional (QuickBook round-trip, BoostComponentService E2E, Celery flow):
# Set GH_TEST_REPO_TOKEN for GitHub-backed tests; unset to skip them.
export GH_TEST_REPO_TOKEN=ghp_...
bash scripts/integration-functional.sh
bash scripts/plugin-functional.sh
```

Each script builds `docker/docker-compose.ci.yml`, waits for health, runs its pytest suite, and tears down the stack.
Expand Down
4 changes: 2 additions & 2 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ SPDX-License-Identifier: BSL-1.0
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.ci.yml** — PostgreSQL + Redis + Weblate stack for integration tests and CI.
- **docker-compose.ci.yml** — PostgreSQL + Redis + Weblate stack for plugin tests and CI.
- **docker-compose.cd.yml** — Weblate-only stack for staging/production (host Postgres, shared Redis).

## Usage

```bash
# CI / integration tests (from repo root):
# CI / plugin tests (from repo root):
docker compose -f docker/docker-compose.ci.yml build
docker compose -f docker/docker-compose.ci.yml up -d

Expand Down
4 changes: 3 additions & 1 deletion docker/docker-compose.ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: BSL-1.0

# CI / integration tests: bundled PostgreSQL + Redis + Weblate (ephemeral Postgres).
# CI / plugin tests: bundled PostgreSQL + Redis + Weblate (ephemeral Postgres).
# CI: docker compose -f docker/docker-compose.ci.yml build && docker compose -f docker/docker-compose.ci.yml up -d
# CD: docker compose -f docker/docker-compose.cd.yml --env-file .env up -d

Expand Down Expand Up @@ -52,6 +52,8 @@ services:
REDIS_HOST: redis
REDIS_PORT: '6379'
CELERY_SINGLE_PROCESS: '1'
BOOST_ENDPOINT_THROTTLE_INFO: 3/minute
BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE: 3/hour
healthcheck:
test: [CMD, curl, -f, http://localhost:8080/healthz/]
interval: 10s
Expand Down
10 changes: 5 additions & 5 deletions docs/boost-weblate-plugin-refactor-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ cppa-weblate-plugin/
|-------------|----------|
| Test coverage | ≥ 90 % (enforced by `--cov-fail-under=90`) |
| CI | GitHub Actions: `pytest`, `ruff check`, coverage gate on every PR |
| Integration test | CI job: `uv pip install weblate "git+https://…/cppa-weblate-plugin@HEAD"` in a Docker-compose stack → smoke-test endpoint + format registration |
| Plugin test | CI job: `uv pip install weblate "git+https://…/cppa-weblate-plugin@HEAD"` in a Docker-compose stack → smoke-test endpoint + format registration |
| CD | CD workflow clones **upstream** `weblate-docker`, edits its `Dockerfile` (install upstream Weblate from PyPI + plugin from `git+https://…/cppa-weblate-plugin@<tag>`, `COPY` `settings-override.py` into the image). `settings-override.py` is versioned in **this** repo. `WEBLATE_ADD_APPS` adds `boost_weblate.endpoint` to `INSTALLED_APPS`; `WEBLATE_FORMATS +=` registers the QuickBook format class. |
| LICENSE | GPL-3.0-or-later in repo root (same license family as Weblate) |
| Runtime deps | All deps declared in `pyproject.toml`; no implicit system deps without docs |
Expand Down Expand Up @@ -108,7 +108,7 @@ Weblate's `urls.py` does **not** auto-discover URL patterns from `INSTALLED_APPS
- `AppConfig.ready()` — programmatically append to `urlpatterns` at startup (simplest for a self-contained plugin), **or**
- `ROOT_URLCONF` override in `settings-override.py` — point to a custom URL conf that imports Weblate's patterns and adds the plugin's `include()`.

**Action:** prototype both approaches in the integration test environment and pick the one that survives Weblate upgrades best. Document the chosen approach in `README.md` so Week 2 implementation can proceed without ambiguity.
**Action:** prototype both approaches in the plugin test environment and pick the one that survives Weblate upgrades best. Document the chosen approach in `README.md` so Week 2 implementation can proceed without ambiguity.

---

Expand Down Expand Up @@ -136,7 +136,7 @@ The `AppConfig`, URL config, and settings registration skeletons are in [Appendi

**Weekly outcome:** **`integration.yml`** is fully working (no partial phases); **`docs/deployment-runbook.md`** written once to match the live CD path; CD script and release tag; fork retired.

## Integration CI — complete in this week
## Plugin CI — complete in this week

- Replace the **`integration.yml`** placeholder with a finished workflow: Docker Compose stack, `uv pip install weblate "git+https://…/cppa-weblate-plugin@HEAD"`, smoke-test format registration and `/boost-endpoint/`.

Expand Down Expand Up @@ -166,8 +166,8 @@ The standalone plugin approach is the only alternative that satisfies all six cr

| ID | Risk | Likelihood | Impact | Mitigation |
|----|------|-----------|--------|------------|
| R1 | `BaseFormat` ABC changes between Weblate releases, breaking `QuickBookFormat` | Low — it is the public extension API | High | Pin Weblate version in `pyproject.toml`; integration CI installs both packages on every PR; bumping the pin is a deliberate, tested action |
| R2 | Weblate removes or renames `WEBLATE_FORMATS` / `WEBLATE_ADD_APPS` Docker env vars | Very low — these are documented deployment knobs | High | Same pin + integration CI; if removed, `settings-override.py` `COPY` path is a fallback requiring only a `Dockerfile` edit |
| R1 | `BaseFormat` ABC changes between Weblate releases, breaking `QuickBookFormat` | Low — it is the public extension API | High | Pin Weblate version in `pyproject.toml`; plugin CI installs both packages on every PR; bumping the pin is a deliberate, tested action |
| R2 | Weblate removes or renames `WEBLATE_FORMATS` / `WEBLATE_ADD_APPS` Docker env vars | Very low — these are documented deployment knobs | High | Same pin + plugin CI; if removed, `settings-override.py` `COPY` path is a fallback requiring only a `Dockerfile` edit |
| R3 | Django major-version upgrade inside Weblate breaks `boost_endpoint` views/serializers | Low — Django REST patterns are stable across majors | Medium | Standard Django upgrade path applies; no Weblate-internal Django usage in the plugin |
| R4 | `git+https://…@<tag>` install fails in air-gapped or restricted network environments | Medium — depends on deployment environment | Medium | Mirror the plugin package to an internal PyPI index if required; the install mechanism is standard pip and switchable |
| R5 | Scope creep requiring Weblate internal API access (e.g. signals, celery tasks) | Low given current requirements | High | Any new requirement that cannot be satisfied via documented extension points is a no-go trigger |
Expand Down
12 changes: 6 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ dev = [
"coverage[toml]>=7.6.0",
"pytest-cov>=7.1.0"
]
integration = [
"pytest>=8.3",
"pytest-timeout>=2.3.1"
]
lint = [
{include-group = "pre-commit"}
]
plugin = [
"pytest>=8.3",
"pytest-timeout>=2.3.1"
]
pre-commit = [
"prek==0.3.13",
"pytest>=8.3"
Expand Down Expand Up @@ -130,9 +130,9 @@ level = "cautious"
unauthorized_licenses = []

[tool.pytest.ini_options]
addopts = ["-m", "not integration"]
addopts = ["-m", "not plugin"]
markers = [
"integration: requires live Weblate stack (Docker Compose) and optional WEBLATE_API_TOKEN"
"plugin: requires live Weblate stack (Docker Compose) and optional WEBLATE_API_TOKEN"
]
python_classes = ["Test*"]
python_files = ["test_*.py", "*_test.py"]
Expand Down
6 changes: 4 additions & 2 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ 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).
- **plugin-smoke.sh** — CI entrypoint for P0 smoke tests (build, start, health-check, test, teardown).
- **plugin-auth.sh** — CI entrypoint for auth and rate-limit tests.
- **plugin-functional.sh** — CI entrypoint for E2E functional tests (optional GitHub repo).

## Usage

```bash
# Run smoke tests locally:
bash scripts/integration-smoke.sh
bash scripts/plugin-smoke.sh

# Source the library for custom workflows:
source scripts/lib/weblate-stack.sh
Expand Down
10 changes: 6 additions & 4 deletions scripts/integration-auth.sh → scripts/plugin-auth.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
# SPDX-License-Identifier: BSL-1.0

# Integration auth test entrypoint.
# Plugin auth test entrypoint.
# Builds the stack, waits for health, creates a token, runs auth tests.
# On exit (success or failure): collects logs and tears down the stack.

Expand Down Expand Up @@ -38,8 +38,10 @@ 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}"
export BOOST_ENDPOINT_THROTTLE_INFO="${BOOST_ENDPOINT_THROTTLE_INFO:-3/minute}"
export BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE="${BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE:-3/hour}"

echo "=== Running auth tests ==="
uv pip install --quiet --system --group integration
python -m pytest --confcutdir=tests/integration --override-ini addopts= \
tests/integration/test_auth.py -v
uv pip install --quiet --system --group plugin
python -m pytest --confcutdir=tests/plugin --override-ini addopts= \
tests/plugin/test_auth.py tests/plugin/test_rate_limit.py -v
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
# SPDX-License-Identifier: BSL-1.0

# Integration functional test entrypoint (P1).
# Plugin functional test entrypoint (P1).
# Builds the stack, waits for health, creates API token, extracts SSH pubkey,
# runs functional tests against a live Weblate instance.

Expand Down Expand Up @@ -58,6 +58,6 @@ else
fi

echo "=== Running functional tests ==="
uv pip install --quiet --system --group integration
python -m pytest --confcutdir=tests/integration --override-ini addopts= \
tests/integration/test_functional.py -v --timeout=300
uv pip install --quiet --system --group plugin
python -m pytest --confcutdir=tests/plugin --override-ini addopts= \
tests/plugin/test_functional.py -v --timeout=300
Loading
Loading