diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..484fb69 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# +# SPDX-License-Identifier: BSL-1.0 + +* @whisper67265 @AuraMindNest diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..c917d99 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# +# SPDX-License-Identifier: BSL-1.0 + +name: Bug report +description: Report something that is broken or incorrect +title: '[Bug]: ' +labels: +- bug +body: +- type: markdown + attributes: + value: | + Thanks for reporting a bug. Please include versions and reproduction steps. + For local test setup, see CONTRIBUTING.md in the repository root. +- type: textarea + id: description + attributes: + label: What happened? + description: Describe the bug and what you expected instead. + placeholder: When I POST to /boost-endpoint/add-or-update/, I get a 500... + validations: + required: true +- type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: Minimal steps to reproduce the behavior. + placeholder: | + 1. docker compose -f docker/docker-compose.ci.yml up -d + 2. curl ... + 3. See error + validations: + required: true +- type: dropdown + id: environment + attributes: + label: Environment + description: Where did you observe the bug? + options: + - Local unit tests (pytest, no Docker) + - Local Docker CI stack (docker-compose.ci.yml) + - Staging deployment + - Production deployment + - Other + validations: + required: true +- type: input + id: plugin-version + attributes: + label: Plugin version + description: Version from pyproject.toml, git tag, or commit SHA + placeholder: 1.0.0 or abc1234 +- type: input + id: weblate-version + attributes: + label: Weblate version + description: PyPI or Docker tag if using the Weblate stack + placeholder: 2026.5 / weblate/weblate:2026.5.0.0 +- type: textarea + id: logs + attributes: + label: Relevant logs or stack traces + description: Paste error output, container logs, or pytest failure snippets. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..53d919e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# +# SPDX-License-Identifier: BSL-1.0 + +blank_issues_enabled: false +contact_links: +- name: Contributing guide + url: https://github.com/cppalliance/cppa-weblate-plugin/blob/develop/CONTRIBUTING.md + about: Local dev setup, tests, and pull request expectations +- name: Maintainer handoff + url: https://github.com/cppalliance/cppa-weblate-plugin/blob/develop/docs/maintainer-handoff.md + about: Weblate coupling points and first-change walkthrough diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..f2777ac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: 2026 Andrew Zhang +# +# SPDX-License-Identifier: BSL-1.0 + +name: Feature request +description: Suggest a new capability or improvement +title: '[Feature]: ' +labels: +- enhancement +body: +- type: markdown + attributes: + value: | + Describe the problem and proposed solution. Note whether the change belongs + in this plugin or upstream Weblate. +- type: textarea + id: problem + attributes: + label: Problem + description: What problem does this solve? Who is affected? + placeholder: Boost translators need ... + validations: + required: true +- type: textarea + id: solution + attributes: + label: Proposed solution + description: How should the plugin address this? + validations: + required: true +- type: dropdown + id: scope + attributes: + label: Scope + description: Where should this change live? + options: + - Plugin only (formats, endpoint, settings_override) + - Upstream Weblate (file an issue there instead) + - Both (plugin workaround until upstream lands) + - Documentation / tooling only + validations: + required: true +- type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other approaches you considered and why they were rejected. +- type: checkboxes + id: areas + attributes: + label: Affected areas + description: Which parts of the plugin might this touch? + options: + - label: QuickBook format (formats/, utils/) + - label: Boost endpoint API (endpoint/) + - label: settings_override / Docker deployment + - label: CI/CD or Weblate version pin diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e3c1203 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,193 @@ + + +# Contributing + +Thank you for contributing to **cppa-weblate-plugin**. This guide covers local setup, testing, and pull-request expectations so you can build and verify changes without tribal knowledge. + +For maintainer roster and escalation, see [MAINTAINERS.md](MAINTAINERS.md). For non-obvious Weblate coupling points, see [docs/maintainer-handoff.md](docs/maintainer-handoff.md). + +## Prerequisites + +| Tool | Version / notes | +|------|-----------------| +| Python | **3.12+** (CI runs 3.12, 3.13, and 3.14) | +| [uv](https://docs.astral.sh/uv/) | Package and venv management | +| Docker + Compose | Required for plugin integration tests (`tests/plugin/`) | +| `GH_TEST_REPO_TOKEN` | Optional classic PAT with `repo` scope for GitHub-backed functional tests | + +## Clone and install + +```bash +git clone https://github.com/cppalliance/cppa-weblate-plugin.git +cd cppa-weblate-plugin +uv venv +source .venv/bin/activate +uv pip install -e '.[dev]' +``` + +## Pre-commit hooks (prek) + +CI lint job runs the same hooks as [`.pre-commit-config.yaml`](.pre-commit-config.yaml) via **prek** (a pre-commit-compatible runner): + +```bash +prek install +prek run --all-files --show-diff-on-failure +``` + +Hooks include Ruff (lint + format), YAML/TOML checks, REUSE license compliance, actionlint, and a default pytest run. Local runs should match [`.github/workflows/ci-lint.yml`](.github/workflows/ci-lint.yml). + +## Unit tests + +Default pytest excludes slow or environment-specific markers (`plugin`, `benchmark`, `fuzz`, `weblate_contract`) — see markers in [`pyproject.toml`](pyproject.toml): + +```bash +pytest +``` + +Run a focused file: + +```bash +pytest tests/test_settings_override.py -v +``` + +### Coverage gate + +CI enforces **90%** coverage on `boost_weblate`: + +```bash +pytest -v --tb=short \ + --cov=boost_weblate \ + --cov-report=term-missing \ + --cov-report=xml:coverage.xml \ + --cov-report=html:htmlcov \ + --cov-fail-under=90 +``` + +Open `htmlcov/index.html` locally to browse line coverage (`coverage.xml`, `htmlcov/`, and `.coverage` are gitignored). + +### Test layout + +| Directory | What it tests | +|-----------|---------------| +| `tests/formats/` | Format adapters and registry | +| `tests/utils/` | QuickBook parse/serialize logic | +| `tests/endpoint/` | HTTP API, serializers, services | +| `tests/plugin/` | Live Weblate Docker stack (see below) | + +Add tests next to the code you touch. Keep `django.setup()`-friendly patterns; the bundled Django test settings intentionally avoid heavy DB or migration suites. + +## Plugin and Docker tests + +Plugin tests require a running Weblate stack built from [`docker/docker-compose.ci.yml`](docker/docker-compose.ci.yml). + +### How settings-override reaches Weblate + +The Docker image copies the plugin settings fragment at build time: + +```dockerfile +# docker/Dockerfile.weblate-plugin +COPY src/boost_weblate/settings_override.py /app/data/settings-override.py +``` + +Weblate's official image `exec()`s `/app/data/settings-override.py` into the same namespace as `weblate.settings_docker`. That file registers `WEBLATE_FORMATS`, appends the endpoint Django app to `INSTALLED_APPS`, and merges throttle rates. See [docs/maintainer-handoff.md](docs/maintainer-handoff.md) for why this coupling exists. + +### Running plugin tests locally + +```bash +# Smoke (P0 — container boot, format registration, URL registration): +bash scripts/plugin-smoke.sh + +# Auth (token auth on protected routes; ping stays public): +bash scripts/plugin-auth.sh + +# Functional (QuickBook round-trip, BoostComponentService E2E, Celery flow): +export GH_TEST_REPO_TOKEN=ghp_... # optional; skips GitHub tests if unset +bash scripts/plugin-functional.sh +``` + +Each script builds the CI Compose stack, waits for health, runs its pytest suite, and tears down. See [`docker/README.md`](docker/README.md) and [`scripts/README.md`](scripts/README.md) for details. + +### Manual Docker stack + +```bash +docker compose -f docker/docker-compose.ci.yml build +docker compose -f docker/docker-compose.ci.yml up -d +# Weblate at http://localhost:8080 (admin / admin) once healthcheck passes +``` + +## Weblate version pin workflow + +PyPI and Docker Weblate versions must stay in sync: + +| File | Pin | +|------|-----| +| [`pyproject.toml`](pyproject.toml) | `Weblate[postgres]==…` | +| [`docker/Dockerfile.weblate-plugin`](docker/Dockerfile.weblate-plugin) | `FROM weblate/weblate:…` | + +Verify locally: + +```bash +bash scripts/check-weblate-pin-sync.sh +``` + +CI runs this in [`.github/workflows/ci-weblate-pin.yml`](.github/workflows/ci-weblate-pin.yml). Scheduled bumps via [`.github/workflows/weblate-pin-bump.yml`](.github/workflows/weblate-pin-bump.yml) also run [`scripts/check-weblate-internal-contract.sh`](scripts/check-weblate-internal-contract.sh) against upstream internals before opening a PR. + +## Parser benchmarks + +QuickBook benchmarks live in [`tests/utils/test_quickbook.py`](tests/utils/test_quickbook.py) (`@pytest.mark.benchmark`). They are excluded from default pytest and pre-commit: + +```bash +uv run --group dev pytest -m benchmark --benchmark-only -v tests/utils/test_quickbook.py +``` + +Compare against the committed baseline and fail on regression (default 30% mean): + +```bash +uv run --group dev pytest -m benchmark --benchmark-only -v \ + -k "not peak_memory" \ + --benchmark-compare=0001 \ + --benchmark-compare-fail=mean:30% \ + tests/utils/test_quickbook.py +``` + +Refresh the baseline after an intentional parser change. **Capture on `ubuntu-latest` (GitHub Actions)** — local machines are often faster and will not match CI. Download the `benchmark-*` artifact from a green `ci-benchmark` run, or re-run on a GitHub-hosted runner: + +```bash +uv run --group dev pytest -m benchmark --benchmark-only \ + -k "not peak_memory" \ + --benchmark-save=baseline tests/utils/test_quickbook.py +``` + +Commit the updated `.benchmarks/Linux-CPython-3.14-64bit/0001_baseline.json`. If CI Python changes, regenerate the baseline (the platform directory name changes). + +CI sets `BENCHMARK_COMPARE_FAIL` (default `mean:30%`) and `BENCHMARK_COMPARE_ENABLED` (default `true`; set `false` for record-only runs). See [`.github/WORKFLOWS.md`](.github/WORKFLOWS.md). + +## Pull request expectations + +1. Branch from **`develop`** (staging integration branch). +2. Keep changes focused; one logical change per PR. +3. Ensure CI is green (lint, test, package, Weblate pin sync, and plugin jobs as applicable). +4. Add **REUSE SPDX headers** to new source files (see existing files for format). +5. Link related issues in the PR description. +6. Respond to review feedback on the PR thread. + +For your first change, follow the [first verified change walkthrough](docs/maintainer-handoff.md#first-verified-change-walkthrough) in the maintainer handoff doc. + +## Where to learn more + +| Topic | Location | +|-------|----------| +| Architecture overview | [README.md](README.md) | +| Weblate coupling / handoff | [docs/maintainer-handoff.md](docs/maintainer-handoff.md) | +| Refactor rationale and plan | [docs/boost-weblate-plugin-refactor-plan.md](docs/boost-weblate-plugin-refactor-plan.md) | +| Staging/production deploy | [docs/deployment-runbook.md](docs/deployment-runbook.md) | +| Boost endpoint API | [docs/boost-endpoint-api.md](docs/boost-endpoint-api.md) | +| HTTP route registration | [docs/plugin-http-routes.md](docs/plugin-http-routes.md) | +| QuickBook grammar coverage | [docs/quickbook-grammar.md](docs/quickbook-grammar.md) | +| CI/CD workflows | [`.github/WORKFLOWS.md`](.github/WORKFLOWS.md) | +| Changelog and SemVer policy | [CHANGELOG.md](CHANGELOG.md) | +| Maintainers and escalation | [MAINTAINERS.md](MAINTAINERS.md) | diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..e7b7675 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,53 @@ + + +# Maintainers + +## Current maintainers + +Andrew Zhang and William Jin are **co-maintainers** with identical qualifications: merge rights, releases, Weblate pin decisions, and production promotion. + +| Name | GitHub | Email | +|------|--------|-------| +| Andrew Zhang | [@whisper67265](https://github.com/whisper67265) | whisper67265@outlook.com | +| William Jin | [@AuraMindNest](https://github.com/AuraMindNest) | william.connects@outlook.com | + +Default code review is configured in [`.github/CODEOWNERS`](.github/CODEOWNERS). + +Technical coupling points for handoff are documented in [docs/maintainer-handoff.md](docs/maintainer-handoff.md). + +## Escalation + +If you need maintainer attention and do not receive a timely response: + +1. Open a [GitHub issue](https://github.com/cppalliance/cppa-weblate-plugin/issues) and mention `@whisper67265` or `@AuraMindNest`. +2. Email either maintainer (`whisper67265@outlook.com` or `william.connects@outlook.com`). +3. If there is no response within **5 business days**, contact [C++ Alliance](https://github.com/cppalliance) org administrators through GitHub (org Settings → Members, or a private security advisory if the matter is sensitive). + +## Community channels + +| Channel | URL | Purpose | +|---------|-----|---------| +| Issues | [github.com/cppalliance/cppa-weblate-plugin/issues](https://github.com/cppalliance/cppa-weblate-plugin/issues) | Bug reports, feature requests | +| Pull requests | [github.com/cppalliance/cppa-weblate-plugin/pulls](https://github.com/cppalliance/cppa-weblate-plugin/pulls) | Code contributions | +| Discussions | *(enable in repo Settings)* | Design questions, announcements | +| Contributing guide | [CONTRIBUTING.md](CONTRIBUTING.md) | Local dev setup, tests, PR expectations | + +### Enabling GitHub Discussions (repo admin) + +Discussions cannot be turned on from committed files. A repository administrator should: + +1. Go to **Settings → General → Features** and enable **Discussions**. +2. Create an **Announcements** category (maintainer-only posts) for release and Weblate pin-bump notices. +3. Optionally add a **General** category for design questions. + +Point contributors to Discussions once enabled; until then, use Issues for design discussion. + +## Related documentation + +- [CONTRIBUTING.md](CONTRIBUTING.md) — end-to-end local dev and test setup +- [docs/maintainer-handoff.md](docs/maintainer-handoff.md) — Weblate coupling points and onboarding walkthrough +- [README.md](README.md) — architecture, CI/CD, configuration reference diff --git a/README.md b/README.md index fdbba4c..38ea079 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ Parser-supported QuickBook constructs, coverage against Boost libraries, and kno Additional formats should follow the same split: a thin class under `src/boost_weblate/formats/` that plugs into Weblate's format APIs, with parsing and reconstruction under `src/boost_weblate/utils/`. +**Governance:** [MAINTAINERS.md](MAINTAINERS.md) · [CONTRIBUTING.md](CONTRIBUTING.md) · [docs/maintainer-handoff.md](docs/maintainer-handoff.md) + ## Quick Start ### Local development @@ -343,6 +345,9 @@ Each script builds `docker/docker-compose.ci.yml`, waits for health, runs its py | Topic | File | Description | |-------|------|-------------| +| Maintainers | [`MAINTAINERS.md`](MAINTAINERS.md) | Roster, escalation | +| Contributing | [`CONTRIBUTING.md`](CONTRIBUTING.md) | Local dev setup, tests, PR expectations | +| Maintainer handoff | [`docs/maintainer-handoff.md`](docs/maintainer-handoff.md) | Weblate coupling points, first-change walkthrough | | Changelog | [`CHANGELOG.md`](CHANGELOG.md) | Version history (Keep a Changelog) | | API stability | [`CHANGELOG.md#deprecation-policy`](CHANGELOG.md#deprecation-policy) | SemVer, deprecation notice period, public API surface | | All env vars | [`.env.example`](.env.example) | Annotated template — copy to `.env` on the deploy server | @@ -360,70 +365,11 @@ Each script builds `docker/docker-compose.ci.yml`, waits for health, runs its py ## Contributing -- **Hooks:** use prek (or classic pre-commit) with `.pre-commit-config.yaml` so local runs match CI (Ruff, YAML/TOML checks, REUSE, actionlint, pytest). - -```bash -uv pip install -e '.[dev]' -prek install -prek run --all-files --show-diff-on-failure -``` - -- **Tests:** add tests next to the code you touch (`tests/formats/`, `tests/utils/`, or `tests/endpoint/`). Keep `django.setup()`-friendly patterns; heavy DB or migration suites are intentionally avoided in the bundled Django test settings. - -- **Python versions:** the package supports CPython **3.12+** (`requires-python` in `pyproject.toml`). CI unit tests run on **3.12**, **3.13**, and **3.14**; lint, package, dependency, and benchmark jobs use a single version (currently **3.14**). - -- **Coverage:** the CI test job enforces 90% minimum on `boost_weblate`. Run locally: - -```bash -pytest -v --tb=short \ - --cov=boost_weblate \ - --cov-report=term-missing \ - --cov-report=xml:coverage.xml \ - --cov-report=html:htmlcov \ - --cov-fail-under=90 -``` - -(`coverage.xml`, `htmlcov/`, and `.coverage` are gitignored; open `htmlcov/index.html` locally to browse line coverage.) - -- **Parser benchmarks:** QuickBook parse-time and memory baselines live in [`tests/utils/test_quickbook.py`](tests/utils/test_quickbook.py) (`@pytest.mark.benchmark`). They are excluded from default pytest runs and pre-commit. Run locally: - -```bash -uv run --group dev pytest -m benchmark --benchmark-only -v tests/utils/test_quickbook.py -``` - -Compare against the committed baseline and fail on regression (default 30% mean): - -```bash -uv run --group dev pytest -m benchmark --benchmark-only -v \ - -k "not peak_memory" \ - --benchmark-compare=0001 \ - --benchmark-compare-fail=mean:30% \ - tests/utils/test_quickbook.py -``` - -Refresh the baseline after an intentional parser change. **Capture on `ubuntu-latest` (GitHub Actions)** — local machines are often faster and will not match CI. Easiest path: download the `benchmark-*` artifact from a green `ci-benchmark` run, copy its `benchmark-results.json` over `0001_baseline.json` (parse-timing benchmarks only), or re-run on a GitHub-hosted runner: - -```bash -uv run --group dev pytest -m benchmark --benchmark-only \ - -k "not peak_memory" \ - --benchmark-save=baseline tests/utils/test_quickbook.py -``` - -Commit the updated `.benchmarks/Linux-CPython-3.14-64bit/0001_baseline.json`. If CI Python changes, regenerate the baseline (the platform directory name changes). - -**Observed baselines** (`ubuntu-latest`, Python 3.14, synthetic documents): - -| Size | Mean `_parse_qbk` time | Segments | -|------|------------------------|----------| -| 100 KB | ~0.042 s | ~1,200 | -| 500 KB | ~0.23 s | ~6,100 | -| 1 MB | ~0.48 s | ~12,200 | -| 1 MB `QuickBookFile.parse` | ~0.55 s | (units ≈ segments) | -| 1 MB peak memory (`tracemalloc`) | ~5.3 MiB | — | +See **[CONTRIBUTING.md](CONTRIBUTING.md)** for end-to-end local setup (uv, prek, pytest, Docker plugin tests, Weblate pin workflow, and PR expectations). -CI sets `BENCHMARK_COMPARE_FAIL` (default `mean:30%`) and `BENCHMARK_COMPARE_ENABLED` (default `true`; set `false` for record-only runs). See [`.github/WORKFLOWS.md`](.github/WORKFLOWS.md). +New maintainers should also read [docs/maintainer-handoff.md](docs/maintainer-handoff.md) for Weblate coupling points and the [first verified change walkthrough](docs/maintainer-handoff.md#first-verified-change-walkthrough). -- **Pull requests:** open PRs against the default branch on GitHub. Keep changes focused; ensure CI is green. Respond to review feedback on the PR thread; for design questions or bug reports, use [Issues](https://github.com/cppalliance/cppa-weblate-plugin/issues). +**Quick reference:** install with `uv pip install -e '.[dev]'`, run hooks with `prek run --all-files`, run unit tests with `pytest`. CI enforces 90% coverage on `boost_weblate` and runs plugin smoke/auth/functional jobs against the Docker CI stack. Parser benchmark details and baseline refresh instructions are in [CONTRIBUTING.md](CONTRIBUTING.md#parser-benchmarks). ## License diff --git a/docs/maintainer-handoff.md b/docs/maintainer-handoff.md new file mode 100644 index 0000000..75a27a9 --- /dev/null +++ b/docs/maintainer-handoff.md @@ -0,0 +1,145 @@ + + +# Maintainer handoff — Weblate coupling points + +This note consolidates the non-obvious integration points between **cppa-weblate-plugin** and upstream Weblate. It explains *why* each coupling exists and *where* to change code when upstream moves. Detailed API, deployment, and grammar docs live in linked files — this document does not duplicate them. + +## 1. `exec()` settings injection + +**File:** [`src/boost_weblate/settings_override.py`](../src/boost_weblate/settings_override.py) + +Weblate's official Docker image evaluates a single optional file after base settings: if `/app/data/settings-override.py` exists, it is compiled and executed with `exec()` in the **same namespace** as `weblate.settings_docker`. The Dockerfile copies our module there: + +```dockerfile +COPY src/boost_weblate/settings_override.py /app/data/settings-override.py +``` + +At runtime this fragment: + +- Sets `WEBLATE_FORMATS` via `weblate_formats_with_plugin_formats()` (delegates core path resolution to [`formats_source.py`](../src/boost_weblate/formats/formats_source.py); see §2). +- Appends `boost_weblate.endpoint.apps.BoostEndpointConfig` to `INSTALLED_APPS` only when it is not already present (avoids duplicate registration if `WEBLATE_ADD_APPS` also lists the endpoint app). +- Merges Boost endpoint throttle rates into `REST_FRAMEWORK`. + +Mutations use `globals().get(...)` because the file runs inside Weblate's settings namespace, not as a normal import during Django bootstrap. Tuple vs list handling for `INSTALLED_APPS` matters: tuple `+=` creates a new object and must be assigned back to `globals()["INSTALLED_APPS"]`; list `+=` mutates in place. + +When imported in unit tests (no Docker namespace), the module still defines `WEBLATE_FORMATS` but skips `INSTALLED_APPS` / `REST_FRAMEWORK` mutations. + +**Further reading:** + +- [README — WEBLATE_FORMATS configuration](../README.md#weblate_formats-configuration) +- [README — WEBLATE_ADD_APPS](../README.md#weblate_add_apps) (do not double-register the endpoint app) + +## 2. FormatsConf AST parsing (strategy layer) + +**Files:** + +- [`src/boost_weblate/formats/formats_source.py`](../src/boost_weblate/formats/formats_source.py) — AST parsing and `FormatsSource` strategy +- [`src/boost_weblate/settings_override.py`](../src/boost_weblate/settings_override.py) — `weblate_formats_with_plugin_formats()` merges core + plugin paths into `WEBLATE_FORMATS` + +`WEBLATE_FORMATS` must include upstream format paths **plus** plugin formats. The naive approach — `import weblate.formats.models` — fails during settings load with `AppRegistryNotReady` because that module pulls in Django ORM classes before the app registry is ready. + +The workaround is a **strategy layer** (`FormatsSource` protocol): + +| Piece | Role | +|-------|------| +| `AstFormatsConfSource` | Default implementation: reads `weblate/formats/models.py` from disk and **AST-parses** `FormatsConf.FORMATS` | +| `resolve_core_weblate_formats()` | Entry point; uses `AstFormatsConfSource` unless a test injects another `FormatsSource` | +| `weblate_formats_with_plugin_formats()` | Appends plugin paths from [`FormatRegistry`](../src/boost_weblate/formats/registry.py), deduplicating against core | + +AST helpers `_parse_formatsconf_formats_ast()` and `_formats_assignment_to_strings()` live in `formats_source.py`. Resolution failures raise `FormatsSourceResolutionError`, wrapped as `RuntimeError` with the pinned Weblate version in the message. + +**When upstream changes:** if Weblate restructures `FormatsConf` (renames the class, moves `FORMATS` off a simple tuple/list assignment, or uses non-literal elements), update the AST helpers in `formats_source.py`. If the upstream layout changes so fundamentally that AST parsing is no longer viable, add a new `FormatsSource` implementation rather than growing `settings_override.py`. + +**Verification:** + +- [`tests/formats/test_weblate_formats_source.py`](../tests/formats/test_weblate_formats_source.py) — AST parse matches live upstream source; `weblate_contract` marker. +- [`tests/test_weblate_internal_contract.py`](../tests/test_weblate_internal_contract.py) — end-to-end `WEBLATE_FORMATS` construction and `real_patterns` acceptance; `weblate_contract` marker. +- [`tests/test_settings_override.py`](../tests/test_settings_override.py) — settings fragment behavior (env parsing, `INSTALLED_APPS` mutation) without Docker. + +Both contract test modules are exercised by [`scripts/check-weblate-internal-contract.sh`](../scripts/check-weblate-internal-contract.sh) on pin-bump PRs. + +## 3. URL registration coupling + +**File:** [`src/boost_weblate/endpoint/weblate_urls_adapter.py`](../src/boost_weblate/endpoint/weblate_urls_adapter.py) + +Weblate does **not** auto-discover URLconfs from arbitrary `INSTALLED_APPS` entries. It builds `weblate.urls.real_patterns` by hand with explicit guards for built-in apps. + +This plugin registers routes in `BoostEndpointConfig.ready()` by appending a `URLResolver` to `real_patterns` after fail-fast layout checks. Registration is idempotent via `functools.lru_cache`. + +**When upstream changes:** if Weblate renames `real_patterns`, changes its type, or moves URL prefix handling, update the adapter and the contract test. + +**Further reading:** [docs/plugin-http-routes.md](plugin-http-routes.md) + +## 4. Weblate version pin story + +Two pins must stay aligned: + +| Location | Example | +|----------|---------| +| [`pyproject.toml`](../pyproject.toml) | `Weblate[postgres]==2026.5` | +| [`docker/Dockerfile.weblate-plugin`](../docker/Dockerfile.weblate-plugin) | `FROM weblate/weblate:2026.5.0.0` | + +Calver mapping lives in [`scripts/weblate-version-map.sh`](../scripts/weblate-version-map.sh). CI enforces sync via [`scripts/check-weblate-pin-sync.sh`](../scripts/check-weblate-pin-sync.sh) ([`ci-weblate-pin.yml`](../.github/workflows/ci-weblate-pin.yml)). + +Scheduled bumps ([`weblate-pin-bump.yml`](../.github/workflows/weblate-pin-bump.yml)) run [`scripts/check-weblate-internal-contract.sh`](../scripts/check-weblate-internal-contract.sh) against the latest PyPI release **before** opening a bump PR. That script runs `pytest -m weblate_contract` on `tests/test_weblate_internal_contract.py` and `tests/formats/test_weblate_formats_source.py` — covering FormatsConf AST parsing, `WEBLATE_FORMATS` construction, and `real_patterns` acceptance. + +**Maintainer action on bump PRs:** review contract-check output. Contract failures are prefixed `[FormatsConf.FORMATS AST]`, `[WEBLATE_FORMATS]`, or `[weblate.urls.real_patterns]` — fix the matching module (`formats_source.py`, `settings_override.py`, or `weblate_urls_adapter.py`) before merging the pin. + +## Documentation index + +| Document | Purpose | +|----------|---------| +| [boost-weblate-plugin-refactor-plan.md](boost-weblate-plugin-refactor-plan.md) | Why a plugin instead of a fork; feasibility and package structure | +| [deployment-runbook.md](deployment-runbook.md) | Staging/production CD, promotion, rollback, health checks | +| [boost-endpoint-api.md](boost-endpoint-api.md) | Request/response reference for `/boost-endpoint/` routes | +| [plugin-http-routes.md](plugin-http-routes.md) | How and why routes register at startup | +| [quickbook-grammar.md](quickbook-grammar.md) | Parser-supported constructs, coverage matrix, limitations | + +Also see [README.md](../README.md) for architecture diagrams, CI/CD overview, and environment variable reference. + +## First verified change walkthrough + +This walkthrough is the recommended **good first change** for a new contributor or maintainer onboarding to the repo. It exercises [`settings_override.py`](../src/boost_weblate/settings_override.py) — the highest-risk module — without modifying Weblate internals or requiring Docker. + +**Goal:** add or extend unit tests for `allowed_clone_hosts()` environment parsing in [`tests/test_settings_override.py`](../tests/test_settings_override.py). + +Note: tests for empty string and comma-separated hosts already exist (`test_allowed_clone_hosts_parses_env`). A new contributor should still run this walkthrough to verify the full dev loop; optional extensions include testing whitespace-only entries or duplicate hosts. + +### Steps + +1. **Branch from `develop`:** + + ```bash + git checkout develop + git pull + git checkout -b handoff/walkthrough-allowed-clone-hosts + ``` + +2. **Review existing tests** in `tests/test_settings_override.py` (`test_allowed_clone_hosts_default`, `test_allowed_clone_hosts_parses_env`). Confirm they cover: + - Default (`BOOST_ALLOWED_CLONE_HOSTS` unset) → `["github.com"]` + - `BOOST_ALLOWED_CLONE_HOSTS=""` → `[]` + - `BOOST_ALLOWED_CLONE_HOSTS="GitHub.com, GitLab.com"` → `["github.com", "gitlab.com"]` + +3. **Optional extension:** add a test for a single host with surrounding whitespace, or document in the PR that existing coverage satisfies the walkthrough. + +4. **Run focused tests:** + + ```bash + pytest tests/test_settings_override.py -v + ``` + +5. **Run pre-commit hooks:** + + ```bash + prek run --all-files --show-diff-on-failure + ``` + +6. **Open a PR** against `develop`. Confirm **`ci-lint`** and **`ci-test`** pass (no Docker jobs required for this change). + +7. **Verification:** repeat steps 1–6 independently on a different small change (e.g. a doc fix or throttle-rate test) to confirm you can run the toolchain end-to-end without assistance. + +A maintainer reviews the PR and confirms the change is ready to merge. diff --git a/pyproject.toml b/pyproject.toml index bb9ad8f..ea60f37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -167,6 +167,8 @@ module-root = "src" source-include = [ ".env.example", "CHANGELOG.md", + "CONTRIBUTING.md", + "MAINTAINERS.md", "docker/**", "docs/**", "LICENSES/**",