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
4 changes: 2 additions & 2 deletions .github/WORKFLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ GitHub Actions and CI/CD helpers for this repository (see [`.github/`](../.githu
| [`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-weblate-pin.yml`](workflows/ci-weblate-pin.yml) | **Weblate version sync** — callable from CI; runs [`scripts/check-weblate-pin-sync.sh`](../scripts/check-weblate-pin-sync.sh) so `pyproject.toml` and `Dockerfile.weblate-plugin` pins match |
| [`workflows/weblate-pin-bump.yml`](workflows/weblate-pin-bump.yml) | Scheduled Weblate pin bump (PyPI + Docker + `uv.lock`) |
| [`workflows/weblate-pin-bump.yml`](workflows/weblate-pin-bump.yml) | Scheduled Weblate pin bump (PyPI + Docker + `uv.lock`); runs **upstream contract check** ([`scripts/check-weblate-internal-contract.sh`](../scripts/check-weblate-internal-contract.sh) `--latest`) before bump/PR |
Comment thread
whisper67265 marked this conversation as resolved.
| [`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 |
Expand Down Expand Up @@ -118,7 +118,7 @@ Weblate is **not** bumped by Dependabot. A single logical release is pinned in t

| Location | Example | Format |
|----------|---------|--------|
| [`pyproject.toml`](../pyproject.toml) | `Weblate[all]==2026.5` | PyPI calver |
| [`pyproject.toml`](../pyproject.toml) | `Weblate[postgres]==2026.5` | PyPI calver |
| [`docker/Dockerfile.weblate-plugin`](../docker/Dockerfile.weblate-plugin) | `FROM weblate/weblate:2026.5.0.0` | Docker fixed tag `YEAR.MONTH.PATCH.BUILD` |

Mapping lives in [`scripts/weblate-version-map.sh`](../scripts/weblate-version-map.sh). CI runs [`scripts/check-weblate-pin-sync.sh`](../scripts/check-weblate-pin-sync.sh) on every PR. [`weblate-pin-bump.yml`](workflows/weblate-pin-bump.yml) opens a PR weekly (Monday 09:00 UTC) when a newer PyPI release has a matching Docker fixed tag.
Expand Down
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ updates:
labels:
- dependencies
- python
# Direct deps only (packaging, Weblate[all]). Transitive lockfile entries are
# Direct deps only (packaging, Weblate[postgres]). Transitive lockfile entries are
# pinned by Weblate and updated via weblate-pin-bump, not independently.
allow:
- dependency-type: direct
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ jobs:
plugin_version = data["project"]["version"]
weblate_version = None
for dep in data["project"]["dependencies"]:
match = re.fullmatch(r"Weblate\[all\]==(.+)", dep)
match = re.fullmatch(r"Weblate(?:\[[^\]]+\])?==(.+)", dep)
if match:
weblate_version = match.group(1)
break
if not weblate_version:
raise SystemExit("Weblate[all]== pin not found in pyproject.toml")
raise SystemExit("Weblate pin not found in pyproject.toml")
print(f"plugin_version={plugin_version}")
print(f"weblate_version={weblate_version}")
print(f"tag=v{plugin_version}")
Expand Down
24 changes: 23 additions & 1 deletion .github/workflows/weblate-pin-bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,29 @@ permissions:
pull-requests: write

jobs:
contract-latest:
name: Weblate upstream contract (latest PyPI)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
# actions/checkout v6.0.2
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
with:
persist-credentials: false
# astral-sh/setup-uv v8.1.0
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39
with:
version: 0.11.12
- name: Install apt dependencies (Weblate venv)
run: sudo ./.github/ci/apt-install
- name: Install plugin dependencies
run: uv sync --frozen --group dev
- name: Verify Weblate internal API contracts (latest PyPI)
run: bash scripts/check-weblate-internal-contract.sh --latest

bump-weblate-pin:
needs: [contract-latest]
name: Bump Weblate pin
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -127,7 +149,7 @@ jobs:
if ! git diff --cached --quiet; then
git commit \
-m "chore(deps): bump Weblate pin to ${target_pypi}" \
-m "PyPI Weblate[all]==${target_pypi}" \
-m "PyPI Weblate[postgres]==${target_pypi}" \
-m "Docker weblate/weblate:${target_docker}"
fi

Expand Down
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.0.0] - 2026-06-11
### Changed

- **Dependencies** — Replaced `Weblate[all]` with `Weblate[postgres]` in `pyproject.toml` (postgres extra required to import `weblate.urls`); removed redundant direct `packaging` pin (still provided by Weblate). Docker deployments are unaffected (full base image unchanged); local/CI installs use a smaller dependency tree.


### Added

Expand All @@ -26,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Rate limiting** — scoped DRF throttles for protected endpoints (`info`: 60/minute; `add-or-update`: 10/hour); `BOOST_ENDPOINT_THROTTLE_INFO` and `BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE` env overrides; HTTP 429 with `Retry-After`.
- **CI pipeline** — umbrella `ci.yml` with lint, test (90% coverage gate), package, dependency audit, Weblate pin sync, and Docker-based plugin smoke/auth/functional jobs.
- **CD pipeline** — staging auto-deploy on `develop` (`cd.yml`); production via `promote-main.yml` (ff-only `develop` → `main`) followed by `main` CD.
- **Weblate version pinning** — `Weblate[all]==…` in `pyproject.toml` synced with Docker `FROM weblate/weblate:…`; enforced by `ci-weblate-pin.yml`; scheduled bumps via `weblate-pin-bump.yml`.
- **Weblate version pinning** — `Weblate[postgres]==…` in `pyproject.toml` synced with Docker `FROM weblate/weblate:…`; enforced by `ci-weblate-pin.yml`; scheduled bumps via `weblate-pin-bump.yml`.
- **Release workflow** — manual `release.yml` tags `main` from `pyproject.toml` version and creates GitHub Releases.

## Deprecation Policy
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ Triggered on push and PR to `main` and `develop`. Calls nine reusable sub-workfl
| `benchmark` | [`.github/workflows/ci-benchmark.yml`](.github/workflows/ci-benchmark.yml) | QuickBook parser benchmarks (`pytest-benchmark`); JSON artifact; optional regression gate vs `.benchmarks/` baseline |
| `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) |
| `weblate-pin` | [`.github/workflows/ci-weblate-pin.yml`](.github/workflows/ci-weblate-pin.yml) | PyPI `Weblate[all]==…` in `pyproject.toml` matches Docker `FROM weblate/weblate:…` (`scripts/check-weblate-pin-sync.sh`) |
| `weblate-pin` | [`.github/workflows/ci-weblate-pin.yml`](.github/workflows/ci-weblate-pin.yml) | PyPI `Weblate[postgres]==…` in `pyproject.toml` matches Docker `FROM weblate/weblate:…` (`scripts/check-weblate-pin-sync.sh`) |
| `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 |
Expand Down
4 changes: 2 additions & 2 deletions docs/deployment-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ The Dockerfile builds an overlay image on a **pinned** `weblate/weblate` tag ali

| File | Example |
|------|---------|
| `pyproject.toml` | `Weblate[all]==2026.5` |
| `pyproject.toml` | `Weblate[postgres]==2026.5` |
| `docker/Dockerfile.weblate-plugin` | `FROM weblate/weblate:2026.5.0.0` |

PyPI uses calver (`2026.5`, `2026.6.1`, …). Docker fixed production tags add patch and build components (`2026.5.0.0`, `2026.6.1.0`). CI enforces the mapping via `scripts/check-weblate-pin-sync.sh`. Bumps are proposed by the `Weblate pin bump` GitHub Actions workflow when both registries have the release.
Expand Down Expand Up @@ -385,7 +385,7 @@ The workflow does not check deploy status or server health.

1. Checks out `main` and reads [`pyproject.toml`](../pyproject.toml):
- Plugin version: `[project].version` (e.g. `1.0.0`)
- Weblate pin: `Weblate[all]==…` (e.g. `2026.5`)
- Weblate pin: `Weblate[postgres]==…` (e.g. `2026.5`)
2. Fails if tag `v<plugin-version>` already exists on `origin` (prevents duplicate releases)
3. Extracts the matching `## [<plugin-version>]` section from [`CHANGELOG.md`](../CHANGELOG.md) (fails if missing, before any tag is pushed)
4. Creates annotated tag `v<plugin-version>` on current `main` HEAD and pushes it
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ classifiers = [
"Topic :: Software Development :: Localization"
]
dependencies = [
"packaging==26.2",
"Weblate[all]==2026.5"
"Weblate[postgres]==2026.5"
]
description = "Standalone Weblate plugin for Boost documentation translation."
keywords = [
Expand Down Expand Up @@ -137,12 +136,13 @@ level = "cautious"
unauthorized_licenses = []

[tool.pytest.ini_options]
addopts = ["-m", "not plugin and not benchmark and not fuzz"]
addopts = ["-m", "not plugin and not benchmark and not fuzz and not weblate_contract"]
markers = [
"benchmark: parser performance benchmarks (slow; excluded from default test runs)",
"plugin: requires live Weblate stack (Docker Compose) and optional WEBLATE_API_TOKEN",
"slow: long-running plugin integration test",
"fuzz: property-based / fuzz tests (excluded from default test runs; pytest -m fuzz)"
"fuzz: property-based / fuzz tests (excluded from default test runs; pytest -m fuzz)",
"weblate_contract: verifies undocumented Weblate internal APIs (pin-bump gate only)"
]
python_classes = ["Test*"]
python_files = ["test_*.py", "*_test.py"]
Expand Down
27 changes: 9 additions & 18 deletions scripts/bump-weblate-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,29 +61,19 @@ if [[ -z "$current" ]]; then
fi

list_weblate_pypi_candidates() {
local releases list_status=0
releases="$(list_modern_weblate_pypi_releases)" || list_status=$?
if [[ $list_status -ne 0 ]]; then
return 1
fi

uv run --with packaging python3 - "$current" "$FORCE_VERSION" <<'PY'
import json
import re
import sys
import urllib.request
from packaging.version import Version

current = Version(sys.argv[1])
force = sys.argv[2]

calver = re.compile(r"^\d{4}\.\d+(?:\.\d+)?$")

def is_modern_calver(name: str) -> bool:
if not calver.match(name):
return False
year = int(name.split(".", 1)[0])
return year >= 2020

with urllib.request.urlopen("https://pypi.org/pypi/Weblate/json") as resp:
data = json.load(resp)

releases = [v for v in data["releases"] if is_modern_calver(v)]
releases.sort(key=Version, reverse=True)
releases = [line.strip() for line in sys.stdin if line.strip()]

if force:
if force not in releases:
Expand All @@ -99,6 +89,7 @@ if not candidates:
for candidate in candidates:
print(candidate)
PY
<<<"$releases"
}

resolve_target() {
Expand Down Expand Up @@ -175,7 +166,7 @@ if [[ "$DRY_RUN" -eq 1 ]]; then
exit 0
fi

sed -i "s/Weblate\\[all\\]==[0-9][0-9.]*/Weblate[all]==${target_pypi}/" "$PYPI_FILE"
sed -i "s/Weblate\\(\\[[^]]*\\]\\)\\?==[0-9][0-9.]*/Weblate[postgres]==${target_pypi}/" "$PYPI_FILE"
sed -i "s|^FROM weblate/weblate:[0-9][0-9.]*|FROM weblate/weblate:${target_docker}|" "$DOCKER_FILE"

(
Expand Down
70 changes: 70 additions & 0 deletions scripts/check-weblate-internal-contract.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# SPDX-License-Identifier: BSL-1.0
#
# Verify plugin assumptions about undocumented Weblate internals
# (FormatsConf.FORMATS AST, WEBLATE_FORMATS, weblate.urls.real_patterns).

set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
# shellcheck source=weblate-version-map.sh
source "${ROOT}/scripts/weblate-version-map.sh"

LATEST=0

usage() {
cat <<'EOF'
Usage: check-weblate-internal-contract.sh [--latest]

(default) Run contract tests against the already-installed Weblate version.
--latest Install the newest modern calver Weblate from PyPI, then run tests.
EOF
}

while [[ $# -gt 0 ]]; do
case "$1" in
--latest)
LATEST=1
shift
;;
-h | --help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done

if [[ "$LATEST" -eq 1 ]]; then
latest_ver="$(latest_modern_weblate_pypi_release)"
echo "Installing latest PyPI Weblate[postgres]==${latest_ver}"
uv pip install "Weblate[postgres]==${latest_ver}"
fi

weblate_version="$(uv run python3 -c 'import importlib.metadata; print(importlib.metadata.version("Weblate"))')"
echo "Weblate version under test: ${weblate_version}"
echo "Contracts checked:"
echo " - FormatsConf.FORMATS AST (weblate/formats/models.py)"
echo " - WEBLATE_FORMATS (weblate_formats_with_plugin_formats)"
echo " - weblate.urls.real_patterns (list accepts URLResolver append)"

set +e
uv run --group dev pytest tests/test_weblate_internal_contract.py -v --tb=short -m weblate_contract
pytest_status=$?
set -e

if [[ "$pytest_status" -ne 0 ]]; then
echo "Weblate internal API contract check failed." >&2
echo "Review pytest output above for which contract broke:" >&2
echo " [FormatsConf.FORMATS AST] | [WEBLATE_FORMATS] | [weblate.urls.real_patterns]" >&2
exit "$pytest_status"
fi

echo "Weblate internal API contract check passed (Weblate ${weblate_version})."
4 changes: 2 additions & 2 deletions scripts/check-weblate-pin-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pypi_ver="$(parse_pypi_weblate_version "$PYPI_FILE")"
docker_tag="$(parse_docker_weblate_tag "$DOCKER_FILE")"

if [[ -z "$pypi_ver" ]]; then
echo "ERROR: could not parse Weblate[all]==… from ${PYPI_FILE}" >&2
echo "ERROR: could not parse Weblate[]==… from ${PYPI_FILE}" >&2
exit 1
fi

Expand All @@ -29,7 +29,7 @@ expected_docker="$(pypi_to_docker_fixed "$pypi_ver")"

if [[ "$docker_tag" != "$expected_docker" ]]; then
echo "ERROR: Weblate pin mismatch between PyPI and Docker base image." >&2
echo " pyproject.toml (PyPI): Weblate[all]==${pypi_ver}" >&2
echo " pyproject.toml (PyPI): Weblate[postgres]==${pypi_ver}" >&2
echo " Dockerfile tag: weblate/weblate:${docker_tag}" >&2
echo " expected Docker fixed tag: weblate/weblate:${expected_docker}" >&2
exit 1
Expand Down
40 changes: 38 additions & 2 deletions scripts/weblate-version-map.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ pypi_to_docker_fixed() {

parse_pypi_weblate_version() {
local file="${1:-pyproject.toml}"
grep -E '^[[:space:]]*"Weblate\[all\]==[0-9][0-9.]+"' "$file" \
grep -E '^[[:space:]]*"Weblate(\[[^]]+\])?==[0-9][0-9.]+"' "$file" \
| head -n1 \
| sed -E 's/.*Weblate\[all\]==([0-9][0-9.]+).*/\1/'
| sed -E 's/.*Weblate(\[[^]]+\])?==([0-9][0-9.]+).*/\2/'
}

parse_docker_weblate_tag() {
Expand All @@ -36,3 +36,39 @@ docker_weblate_tag_exists() {
"https://hub.docker.com/v2/repositories/weblate/weblate/tags/${tag}/")"
[[ "$code" == "200" ]]
}

# Modern calver Weblate releases from PyPI (newest first, one per line).
list_modern_weblate_pypi_releases() {
uv run --with packaging python3 - <<'PY'
import json
import re
import sys
import urllib.request
from packaging.version import Version

calver = re.compile(r"^\d{4}\.\d+(?:\.\d+)?$")

def is_modern_calver(name: str) -> bool:
if not calver.match(name):
return False
year = int(name.split(".", 1)[0])
return year >= 2020

with urllib.request.urlopen(
"https://pypi.org/pypi/Weblate/json", timeout=30
) as resp:
data = json.load(resp)

releases = [v for v in data["releases"] if is_modern_calver(v)]
if not releases:
print("ERROR: no modern calver Weblate releases found on PyPI", file=sys.stderr)
raise SystemExit(1)

for version in sorted(releases, key=Version, reverse=True):
print(version)
PY
}

latest_modern_weblate_pypi_release() {
list_modern_weblate_pypi_releases | head -n1
}
Loading
Loading