Skip to content

Commit db330b6

Browse files
authored
Weblate outbound POST JSON schema + schema-driven bats (#58)
* Weblate outbound POST JSON schema + schema-driven bats * fix the coderabbitai review comments * fix the first reviewer comments
1 parent 9c9a3b3 commit db330b6

11 files changed

Lines changed: 243 additions & 8 deletions

.github/workflows/lint.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ jobs:
1818
with:
1919
persist-credentials: false
2020

21-
- name: ShellCheck and actionlint
21+
- name: Install Python for schema validation
22+
if: runner.os == 'Linux'
23+
run: sudo apt-get update && sudo apt-get install -y python3-venv python3-pip
24+
25+
- name: ShellCheck, actionlint, and JSON Schema
2226
run: scripts/lint.sh
2327

2428
test:
@@ -31,7 +35,7 @@ jobs:
3135
persist-credentials: false
3236

3337
- name: Install test dependencies
34-
run: sudo apt-get update && sudo apt-get install -y bats jq curl
38+
run: sudo apt-get update && sudo apt-get install -y bats jq curl python3-pip python3-venv
3539

3640
- name: Run tests
3741
run: make test

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,25 @@ are a separate namespace — see [README](README.md#releases) and
1111

1212
## [Unreleased]
1313

14+
### Added
15+
16+
- JSON Schema for the outbound Weblate add-or-update POST request:
17+
[weblate-add-or-update.request.schema.json](docs/schemas/weblate-add-or-update.request.schema.json)
18+
(`organization`, `version`, `extensions`, `add_or_update`).
19+
- Documented Weblate success response bodies in
20+
[endpoint-contract.md](docs/endpoint-contract.md): HTTP **202** (`task_id`),
21+
HTTP **200** (`status: "ok"`).
22+
- Lint/CI schema checks via pinned `check-jsonschema` (metaschema + fixture
23+
validation); bats validates captured Weblate request bodies against the schema
24+
and asserts success response fields.
25+
1426
### Changed
1527

1628
- Renamed and expanded operator quick reference to
1729
[GETTING-STARTED.md](docs/GETTING-STARTED.md): end-to-end walkthrough with
1830
per-step verification and create-tag coverage.
31+
- [endpoint-contract.md](docs/endpoint-contract.md) Outbound Weblate section:
32+
request schema is now the source of truth for payload fields.
1933

2034
## [1.0.0] - 2026-07-07
2135

docs/endpoint-contract.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Semver applies to documented changes in:
5252
| `repository_dispatch` **`event_type`** values | `add-submodules`, `start-translation`, `sync-translation` |
5353
| **`client_payload`** field names, optionality, semantics | [README](../README.md) workflow tables; sections below |
5454
| Dispatch HTTP contract | URL, auth headers, success = HTTP **204** |
55-
| Outbound Weblate POST | `organization`, `version`, `extensions`, `add_or_update`; success **200** or **202** |
55+
| Outbound Weblate POST | Request schema [weblate-add-or-update.request.schema.json](schemas/weblate-add-or-update.request.schema.json); success **200** or **202** |
5656
| Shell batch return codes **0 / 1 / 2** and job collapse | [ARCHITECTURE §6](ARCHITECTURE.md#6-shell-return-codes) — code **2** never propagates to GitHub Actions step exit |
5757
| Branch/path constants affecting behavior | `MASTER_BRANCH`, `LOCAL_BRANCH_PREFIX`, `TRANSLATION_BRANCH_PREFIX`, `WEBLATE_ENDPOINT_PATH` in [`env.sh`](../.github/workflows/assets/env.sh) |
5858

@@ -134,3 +134,29 @@ submodule pointer updates, and Boost release refs in `client_payload.version`.
134134
| Auth | `Authorization: Token {WEBLATE_TOKEN}` |
135135
| Invoked by | `.github/workflows/start-translation.yml` (`start-local` job) |
136136
| Success | HTTP **200** or **202** (async accepted) |
137+
138+
### Payload
139+
140+
Field names, types, and constraints are defined by
141+
**[weblate-add-or-update.request.schema.json](schemas/weblate-add-or-update.request.schema.json)**
142+
(source of truth). Summary:
143+
144+
| Field | Shape |
145+
| ----- | ----- |
146+
| `organization` | Non-empty string (`MODULE_ORG`) |
147+
| `version` | Non-empty string (Boost libs ref) |
148+
| `extensions` | Array of dot-prefixed strings (may be `[]`) |
149+
| `add_or_update` | Object: language code → non-empty array of submodule basenames |
150+
151+
Built by `trigger_weblate` in `.github/workflows/assets/translation.sh`. The call is
152+
omitted when `add_or_update` would be empty.
153+
154+
### Success response bodies
155+
156+
| HTTP status | Body |
157+
| ----------- | ---- |
158+
| **202** | JSON object with required string `task_id` (async acceptance) |
159+
| **200** | JSON object with required string `status` equal to `"ok"` (sync completion) |
160+
161+
Orchestration treats both statuses as success and prints the response body to logs;
162+
it does not otherwise act on response fields.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://github.com/cppalliance/boost-docs-translation/docs/schemas/weblate-add-or-update.request.schema.json",
4+
"title": "Weblate add-or-update request",
5+
"description": "Outbound POST body built by trigger_weblate for WEBLATE_ENDPOINT_PATH.",
6+
"type": "object",
7+
"additionalProperties": false,
8+
"required": ["organization", "version", "extensions", "add_or_update"],
9+
"properties": {
10+
"organization": {
11+
"type": "string",
12+
"minLength": 1,
13+
"description": "GitHub org hosting library mirror repos (MODULE_ORG)."
14+
},
15+
"version": {
16+
"type": "string",
17+
"minLength": 1,
18+
"description": "Boost libs ref (e.g. develop or boost-1.90.0)."
19+
},
20+
"extensions": {
21+
"type": "array",
22+
"description": "Dot-prefixed file extensions filter for Weblate; empty means no filter.",
23+
"items": {
24+
"type": "string",
25+
"pattern": "^\\..+"
26+
}
27+
},
28+
"add_or_update": {
29+
"type": "object",
30+
"minProperties": 1,
31+
"description": "Map of language code to non-empty arrays of submodule basenames.",
32+
"propertyNames": {
33+
"type": "string",
34+
"pattern": "^[A-Za-z]{2,3}([_-][A-Za-z0-9]{2,8})*$"
35+
},
36+
"additionalProperties": {
37+
"type": "array",
38+
"minItems": 1,
39+
"items": {
40+
"type": "string",
41+
"pattern": "^[a-z][a-z0-9._-]*$"
42+
}
43+
}
44+
}
45+
}
46+
}

scripts/ensure_check_jsonschema.sh

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env bash
2+
# Bootstrap pinned check-jsonschema into .cache/ (shared by lint.sh and test helpers).
3+
# Safe to source: does not enable set -euo (callers own shell options).
4+
# shellcheck shell=bash
5+
6+
CHECK_JSONSCHEMA_VERSION="${CHECK_JSONSCHEMA_VERSION:-0.37.4}"
7+
8+
ensure_check_jsonschema() {
9+
local root version cache_dir venv_dir bin marker
10+
root="${REPO_ROOT:-}"
11+
if [[ -z "$root" ]]; then
12+
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
13+
fi
14+
version="$CHECK_JSONSCHEMA_VERSION"
15+
# Reject path separators / .. so version cannot escape cache_dir via venv_dir.
16+
case "$version" in
17+
'' | */* | *\\* | *..*)
18+
echo "ensure_check_jsonschema: invalid CHECK_JSONSCHEMA_VERSION: ${version}" >&2
19+
return 1
20+
;;
21+
esac
22+
cache_dir="$root/.cache/check-jsonschema"
23+
venv_dir="$cache_dir/${version}"
24+
bin="$venv_dir/bin/check-jsonschema"
25+
marker="$venv_dir/.installed"
26+
27+
if [[ -x "$bin" && -f "$marker" ]]; then
28+
CHECK_JSONSCHEMA_BIN="$bin"
29+
export CHECK_JSONSCHEMA_BIN
30+
return 0
31+
fi
32+
33+
if ! command -v python3 >/dev/null 2>&1; then
34+
echo "ensure_check_jsonschema: python3 is required" >&2
35+
return 1
36+
fi
37+
38+
echo "ensure_check_jsonschema: installing check-jsonschema==${version} into ${venv_dir}..." >&2
39+
rm -rf "$venv_dir"
40+
python3 -m venv "$venv_dir" || return 1
41+
"$venv_dir/bin/pip" install --disable-pip-version-check --quiet \
42+
"check-jsonschema==${version}" || return 1
43+
if [[ ! -x "$bin" ]]; then
44+
echo "ensure_check_jsonschema: expected binary missing at $bin" >&2
45+
return 1
46+
fi
47+
printf '%s\n' "$version" >"$marker"
48+
CHECK_JSONSCHEMA_BIN="$bin"
49+
export CHECK_JSONSCHEMA_BIN
50+
}
51+
52+
# When sourced, define the function; when executed, run it and print the bin path.
53+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
54+
set -euo pipefail
55+
ensure_check_jsonschema
56+
printf '%s\n' "$CHECK_JSONSCHEMA_BIN"
57+
fi

scripts/lint.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,19 @@ echo "lint: actionlint ${ACTIONLINT_VERSION} ($("$ACTIONLINT_BIN" -version | hea
177177
tests/helpers/*.bash
178178

179179
"$ACTIONLINT_BIN" -color
180+
181+
# shellcheck source=scripts/ensure_check_jsonschema.sh
182+
source "$ROOT/scripts/ensure_check_jsonschema.sh"
183+
ensure_check_jsonschema
184+
185+
echo "lint: check-jsonschema ${CHECK_JSONSCHEMA_VERSION}" >&2
186+
shopt -s nullglob
187+
schema_files=(docs/schemas/*.schema.json)
188+
if [[ ${#schema_files[@]} -eq 0 ]]; then
189+
echo "lint: no JSON Schema files under docs/schemas/" >&2
190+
exit 1
191+
fi
192+
"$CHECK_JSONSCHEMA_BIN" --check-metaschema "${schema_files[@]}"
193+
"$CHECK_JSONSCHEMA_BIN" --schemafile \
194+
docs/schemas/weblate-add-or-update.request.schema.json \
195+
tests/helpers/fixtures/weblate-add-or-update-request-valid.json

scripts/test.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,9 @@ ensure_bats() {
3535
}
3636

3737
ensure_bats
38+
39+
# shellcheck source=scripts/ensure_check_jsonschema.sh
40+
source "$ROOT/scripts/ensure_check_jsonschema.sh"
41+
ensure_check_jsonschema
42+
3843
"$BATS_BIN" tests/
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"organization": "testorg",
3+
"version": "develop",
4+
"extensions": [".adoc"],
5+
"add_or_update": {
6+
"en": ["algorithm", "system"]
7+
}
8+
}

tests/helpers/schema.bash

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# shellcheck shell=bash
2+
# JSON Schema helpers for bats (check-jsonschema + request/response extractors).
3+
# shellcheck disable=SC1091
4+
5+
SCHEMAS_DIR="${SCHEMAS_DIR:-$REPO_ROOT/docs/schemas}"
6+
7+
# shellcheck source=../../scripts/ensure_check_jsonschema.sh
8+
source "$REPO_ROOT/scripts/ensure_check_jsonschema.sh"
9+
10+
# Validate a JSON string against a schema file. Writes a temp instance file.
11+
validate_json_against_schema() {
12+
local json="$1" schema_path="$2" tmp
13+
ensure_check_jsonschema || return 1
14+
tmp="$(mktemp)"
15+
printf '%s\n' "$json" >"$tmp"
16+
"$CHECK_JSONSCHEMA_BIN" --schemafile "$schema_path" "$tmp"
17+
local rc=$?
18+
rm -f "$tmp"
19+
return "$rc"
20+
}
21+
22+
# Extract POST body from the Weblate mock request log (BODY_START … BODY_END).
23+
extract_weblate_request_body_from_log() {
24+
local log_file="$1"
25+
sed -n '/^BODY_START$/,/^BODY_END$/p' "$log_file" | sed '1d;$d'
26+
}
27+
28+
# Extract the last JSON object from a stderr/log file (pretty-printed or one-line).
29+
extract_json_object_from_log() {
30+
local log_file="$1"
31+
python3 -c '
32+
import json, sys
33+
text = open(sys.argv[1], encoding="utf-8", errors="replace").read()
34+
decoder = json.JSONDecoder()
35+
candidates = []
36+
i = 0
37+
n = len(text)
38+
while i < n:
39+
start = text.find("{", i)
40+
if start < 0:
41+
break
42+
try:
43+
obj, end = decoder.raw_decode(text, start)
44+
except json.JSONDecodeError:
45+
i = start + 1
46+
continue
47+
if isinstance(obj, dict):
48+
candidates.append(obj)
49+
i = end
50+
for obj in reversed(candidates):
51+
print(json.dumps(obj))
52+
sys.exit(0)
53+
sys.exit(1)
54+
' "$log_file"
55+
}

tests/helpers/test_helper.bash

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@ source "$_HELPER_DIR/common.bash"
1010
source "$_HELPER_DIR/git_fixtures.bash"
1111
# shellcheck source=tests/helpers/http_mock.bash
1212
source "$_HELPER_DIR/http_mock.bash"
13+
# shellcheck source=tests/helpers/schema.bash
14+
source "$_HELPER_DIR/schema.bash"
1315

1416
unset _HELPER_DIR

0 commit comments

Comments
 (0)