From fd0079622ce9e5be46440723ad3d03a0839b0476 Mon Sep 17 00:00:00 2001 From: whisper67265 Date: Thu, 30 Jul 2026 13:19:08 -0600 Subject: [PATCH 1/3] remove the silent two-library default from trigger-add-submodules.sh --- CHANGELOG.md | 7 ++ docs/GETTING-STARTED.md | 19 ++-- docs/endpoint-contract.md | 2 +- scripts/trigger-add-submodules.sh | 18 ++-- scripts/trigger-dispatch-common.sh | 4 +- tests/helpers/http_mock.bash | 111 +++++++++++++++++++++ tests/test_getting_started.bats | 4 + tests/test_trigger_add_submodules.bats | 42 ++++++++ tests/test_trigger_dispatch_common.bats | 116 ++++++++++++++++++++++ tests/test_trigger_start_translation.bats | 55 ++++++++++ 10 files changed, 356 insertions(+), 22 deletions(-) mode change 100644 => 100755 scripts/trigger-add-submodules.sh create mode 100644 tests/test_trigger_add_submodules.bats create mode 100644 tests/test_trigger_dispatch_common.bats create mode 100644 tests/test_trigger_start_translation.bats diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fdcefd..ca9d518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,9 +36,16 @@ are a separate namespace — see [README](README.md#releases) and per-step verification and create-tag coverage. - [endpoint-contract.md](docs/endpoint-contract.md) Outbound Weblate section: request schema is now the source of truth for payload fields. +- `scripts/trigger-add-submodules.sh` now requires `--submodules`; the + `unordered, json` script default is removed so the operator script cannot + silently diverge from raw API auto-discovery. +- Bats coverage for `scripts/trigger-dispatch-common.sh`, `trigger-add-submodules.sh`, + and `trigger-start-translation.sh`. ### Fixed +- `build_dispatch_json` in `trigger-dispatch-common.sh`: avoid jq 1.7 naming + collision with the built-in `pairs` function. - Clone and finalize steps now fail fast: **`clone_repo`**, **`sync_translations_branch`**, and **`finalize_translations_*`** propagate non-zero status instead of continuing with incomplete state. diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index 1e4fd2c..7bf8837 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -66,7 +66,8 @@ linked README sections for scope, format, and which workflows consume each value ## 1. Local trigger setup (optional) -To fire dispatches from a clone of this repo instead of the GitHub API or UI: +To fire dispatches from a clone of this repo instead of the GitHub API +(`POST …/dispatches` or `gh api`): ```bash cp .env.example .env # set GH_TOKEN (GITHUB_TOKEN is also accepted) @@ -117,14 +118,12 @@ scripts/trigger-add-submodules.sh \ ``` - Omit **`--lang-codes`** to use repository variable **`LANG_CODES`**. -- Omit **`--submodules`** to use the script default **`DEFAULT_SUBMODULES`** - (`unordered, json`; see - [trigger-add-submodules.sh](../scripts/trigger-add-submodules.sh#L40)) — **not** - auto-discovery from **`boostorg/boost`**. The script substitutes that default - before building the payload - ([`SUBMODULES` assignment](../scripts/trigger-add-submodules.sh#L99)), so `client_payload.submodules` - is never omitted. Full discovery runs only when the workflow receives a dispatch - **without** a `submodules` field (raw API / GitHub UI). +- **`--submodules` is required** when using this script (comma-separated library + names). To process all Boost libraries, omit `submodules` from a **raw** + `repository_dispatch` payload sent via the GitHub API (for example + `POST …/dispatches` or `gh api repos/{owner}/{repo}/dispatches`) — the workflow + then auto-discovers the full list from **`boostorg/boost`** `.gitmodules` via + **`resolve_add_submodules_names`**. ### Expected outcome @@ -238,7 +237,7 @@ Runs automatically **daily at 00:00 UTC** (`0 0 * * *`) or on manual dispatch. | Item | Value | | -------- | --------------------------------------------------------------------- | | Workflow | [`.github/workflows/sync-translation.yml`](../.github/workflows/sync-translation.yml) | -| Script | **None** — use the dispatches API, GitHub UI, or the curl example below | +| Script | **None** — use the dispatches API or the curl example below | | Trigger | `repository_dispatch` with `event_type: sync-translation` (no `client_payload`) | ### Trigger (JSON) diff --git a/docs/endpoint-contract.md b/docs/endpoint-contract.md index 5829796..860c4c4 100644 --- a/docs/endpoint-contract.md +++ b/docs/endpoint-contract.md @@ -101,7 +101,7 @@ submodule pointer updates, and Boost release refs in `client_payload.version`. | Workflow | `.github/workflows/add-submodules.yml` | | Body shape | `{"event_type":"add-submodules","client_payload":{...}}` | | `client_payload` | All optional: `version`, `submodules` (list-like string), `lang_codes` (comma-separated). See [README](../README.md). | -| Script | `scripts/trigger-add-submodules.sh` builds JSON with `jq` or Python; omits empty optional fields. Both trigger scripts source shared [`scripts/trigger-dispatch-common.sh`](../scripts/trigger-dispatch-common.sh). | +| Script | `scripts/trigger-add-submodules.sh` builds JSON with `jq` or Python; omits empty optional fields; **`--submodules` is required** on the script even though the API field is optional. Both trigger scripts source shared [`scripts/trigger-dispatch-common.sh`](../scripts/trigger-dispatch-common.sh). | ### `event_type: start-translation` diff --git a/scripts/trigger-add-submodules.sh b/scripts/trigger-add-submodules.sh old mode 100644 new mode 100755 index 346ccf9..c7f9882 --- a/scripts/trigger-add-submodules.sh +++ b/scripts/trigger-add-submodules.sh @@ -12,7 +12,7 @@ # # Usage: # scripts/trigger-add-submodules.sh [--repo OWNER/NAME] [--token PAT] \ -# [--version REF] [--submodules 'a, b'] [--lang-codes zh_Hans,ja] +# --submodules 'a, b' [--version REF] [--lang-codes zh_Hans,ja] # # If --repo is omitted: GITHUB_REPOSITORY, then git origin, then DEFAULT_REPO below. @@ -34,18 +34,13 @@ source "$_ASSETS_DIR/lib.sh" source "$_REPO_ROOT/scripts/trigger-dispatch-common.sh" unset _REPO_ROOT _ASSETS_DIR -# --------------------------------------------------------------------------- -# Typical run — edit these. CLI flags override (except --token uses env/PAT). -# --------------------------------------------------------------------------- -DEFAULT_SUBMODULES="unordered, json" - usage() { cat <<'EOF' Trigger add-submodules.yml via repository_dispatch (POST .../dispatches). Usage: scripts/trigger-add-submodules.sh [--repo OWNER/NAME] [--token PAT] \ - [--version REF] [--submodules 'a, b'] [--lang-codes zh_Hans,ja] + --submodules 'a, b' [--version REF] [--lang-codes zh_Hans,ja] Requires: curl; jq or Python 3 (python3 / python) Auth: .env (GH_TOKEN), GH_TOKEN / GITHUB_TOKEN in env, or --token (needs repo scope on the target). @@ -54,7 +49,7 @@ Options: --repo OWNER/REPO Target repository (default: GITHUB_REPOSITORY, then origin, then DEFAULT_REPO) --token PAT GitHub token --version REF Boost ref; default DEFAULT_VERSION in script - --submodules LIST default DEFAULT_SUBMODULES in script + --submodules LIST comma-separated library names (required) --lang-codes CSV optional; omit → workflow uses repo vars.LANG_CODES EOF } @@ -86,6 +81,12 @@ while [[ $# -gt 0 ]]; do esac done +if [[ -z "$SUBMODULES" ]]; then + echo "error: --submodules is required (comma-separated library names)" >&2 + usage >&2 + exit 1 +fi + require_curl || exit 1 TOKEN="$(resolve_trigger_token "$TOKEN")" || exit 1 @@ -96,7 +97,6 @@ REPO="$(resolve_trigger_repo "$REPO")" || { } VERSION="${VERSION:-$DEFAULT_VERSION}" -SUBMODULES="${SUBMODULES:-$DEFAULT_SUBMODULES}" validate_event_type "$EVENT_ADD_SUBMODULES" diff --git a/scripts/trigger-dispatch-common.sh b/scripts/trigger-dispatch-common.sh index 9579b5a..679a47f 100644 --- a/scripts/trigger-dispatch-common.sh +++ b/scripts/trigger-dispatch-common.sh @@ -75,12 +75,12 @@ build_dispatch_json() { json_pairs="$(jq -n --argjson arr "$json_pairs" --arg k "$key" --arg v "$val" \ '$arr + [{key: $k, value: $v}]')" done - jq -n --arg event_type "$event_type" --argjson pairs "$json_pairs" \ + jq -n --arg event_type "$event_type" --argjson kv_pairs "$json_pairs" \ '{ event_type: $event_type, client_payload: ( {} - | reduce pairs[] as $p ( + | reduce $kv_pairs[] as $p ( .; if ($p.value | length) > 0 then . + {($p.key): $p.value} else . end ) diff --git a/tests/helpers/http_mock.bash b/tests/helpers/http_mock.bash index b464d29..1ccc4e8 100644 --- a/tests/helpers/http_mock.bash +++ b/tests/helpers/http_mock.bash @@ -145,3 +145,114 @@ restore_curl_stub() { fi unset CURL_WRAPPER_DIR REAL_CURL _CURL_ORIG_PATH MOCK_CURL_TIMEOUT MOCK_CURL_EXIT } + +# Intercept GitHub repository_dispatch POST (no real network). +install_dispatch_curl_stub() { + _CURL_ORIG_PATH="$PATH" + local wrapper_dir + wrapper_dir="$(mktemp -d)" + REAL_CURL="$(command -v curl)" + export REAL_CURL + MOCK_DISPATCH_REQUEST_LOG="${MOCK_DISPATCH_REQUEST_LOG:-$(mktemp)}" + export MOCK_DISPATCH_REQUEST_LOG + MOCK_DISPATCH_STATUS="${MOCK_DISPATCH_STATUS:-204}" + export MOCK_DISPATCH_STATUS + MOCK_DISPATCH_RESPONSE_BODY="${MOCK_DISPATCH_RESPONSE_BODY:-}" + export MOCK_DISPATCH_RESPONSE_BODY + cat >"$wrapper_dir/curl" <<'EOF' +#!/usr/bin/env bash +if [[ -n "${MOCK_CURL_EXIT:-}" ]]; then + echo "mock curl: simulated exit ${MOCK_CURL_EXIT}" >&2 + exit "$MOCK_CURL_EXIT" +fi +if [[ "${MOCK_CURL_TIMEOUT:-}" == "1" ]]; then + echo "mock curl: simulated timeout" >&2 + exit 28 +fi +args=("$@") +url="" +method="" +body_file="" +body_inline="" +out_file="" +write_out="" +i=0 +while [[ $i -lt ${#args[@]} ]]; do + arg="${args[$i]}" + case "$arg" in + -X) + method="${args[$((i + 1))]}" + i=$((i + 2)) + ;; + -d) + next="${args[$((i + 1))]}" + if [[ -f "$next" ]]; then + body_file="$next" + else + body_inline="$next" + fi + i=$((i + 2)) + ;; + -o) + out_file="${args[$((i + 1))]}" + i=$((i + 2)) + ;; + -w) + write_out="${args[$((i + 1))]}" + i=$((i + 2)) + ;; + http://*|https://*) + url="$arg" + i=$((i + 1)) + ;; + *) + i=$((i + 1)) + ;; + esac +done +if [[ "$url" == *"api.github.com/repos/"*/dispatches && "$method" == "POST" ]]; then + { + echo "URL=$url" + echo "METHOD=$method" + if [[ -n "$body_file" ]]; then + echo "BODY_START" + cat "$body_file" + echo "" + echo "BODY_END" + elif [[ -n "$body_inline" ]]; then + echo "BODY_START" + printf '%s\n' "$body_inline" + echo "BODY_END" + fi + } >"$MOCK_DISPATCH_REQUEST_LOG" + if [[ -n "$out_file" ]]; then + if [[ -n "${MOCK_DISPATCH_RESPONSE_BODY:-}" ]]; then + printf '%s' "$MOCK_DISPATCH_RESPONSE_BODY" >"$out_file" + else + : >"$out_file" + fi + fi + if [[ -n "$write_out" ]]; then + printf '%s' "${MOCK_DISPATCH_STATUS:-204}" + fi + exit 0 +fi +exec "$REAL_CURL" "$@" +EOF + chmod +x "$wrapper_dir/curl" + CURL_WRAPPER_DIR="$wrapper_dir" + export PATH="$wrapper_dir:$PATH" +} + +restore_dispatch_curl_stub() { + restore_curl_stub + if [[ -n "${MOCK_DISPATCH_REQUEST_LOG:-}" && -f "$MOCK_DISPATCH_REQUEST_LOG" ]]; then + rm -f "$MOCK_DISPATCH_REQUEST_LOG" + fi + unset MOCK_DISPATCH_REQUEST_LOG MOCK_DISPATCH_STATUS MOCK_DISPATCH_RESPONSE_BODY +} + +extract_dispatch_request_body() { + local log="${1:?}" + awk '/^BODY_START$/{found=1; next} /^BODY_END$/{found=0} found' "$log" +} diff --git a/tests/test_getting_started.bats b/tests/test_getting_started.bats index da78776..aa7e358 100644 --- a/tests/test_getting_started.bats +++ b/tests/test_getting_started.bats @@ -80,3 +80,7 @@ setup() { @test "GETTING-STARTED.md documents dispatch-order guard" { grep -q 'add-submodules first' "$DOC" } + +@test "GETTING-STARTED.md does not document DEFAULT_SUBMODULES script default" { + ! grep -q 'DEFAULT_SUBMODULES' "$DOC" +} diff --git a/tests/test_trigger_add_submodules.bats b/tests/test_trigger_add_submodules.bats new file mode 100644 index 0000000..6695ce2 --- /dev/null +++ b/tests/test_trigger_add_submodules.bats @@ -0,0 +1,42 @@ +#!/usr/bin/env bats + +setup() { + ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + SCRIPT="$ROOT/scripts/trigger-add-submodules.sh" + # shellcheck source=tests/helpers/http_mock.bash + source "$BATS_TEST_DIRNAME/helpers/http_mock.bash" +} + +teardown() { + restore_dispatch_curl_stub +} + +@test "trigger-add-submodules: --help exits 0" { + run "$SCRIPT" --help + [ "$status" -eq 0 ] + [[ "$output" == *"trigger-add-submodules.sh"* ]] +} + +@test "trigger-add-submodules: missing --submodules errors with usage" { + run "$SCRIPT" --repo owner/repo + [ "$status" -eq 1 ] + [[ "$output" == *"error: --submodules is required"* ]] + [[ "$output" == *"Usage:"* ]] +} + +@test "trigger-add-submodules: empty --submodules errors with usage" { + run "$SCRIPT" --repo owner/repo --submodules '' + [ "$status" -eq 1 ] + [[ "$output" == *"error: --submodules is required"* ]] + [[ "$output" == *"Usage:"* ]] +} + +@test "trigger-add-submodules: explicit --submodules dispatches payload" { + install_dispatch_curl_stub + run "$SCRIPT" --repo owner/repo --token fake-token --submodules 'a, b' + [ "$status" -eq 0 ] + [[ "$output" == *"Dispatched add-submodules to owner/repo (HTTP 204)"* ]] + body="$(extract_dispatch_request_body "$MOCK_DISPATCH_REQUEST_LOG")" + echo "$body" | jq -e '.event_type == "add-submodules"' >/dev/null + echo "$body" | jq -e '.client_payload.submodules == "a, b"' >/dev/null +} diff --git a/tests/test_trigger_dispatch_common.bats b/tests/test_trigger_dispatch_common.bats new file mode 100644 index 0000000..947106a --- /dev/null +++ b/tests/test_trigger_dispatch_common.bats @@ -0,0 +1,116 @@ +#!/usr/bin/env bats + +setup() { + ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + # shellcheck source=tests/helpers/http_mock.bash + source "$BATS_TEST_DIRNAME/helpers/http_mock.bash" + # shellcheck source=/dev/null + source "$ROOT/scripts/trigger-dispatch-common.sh" +} + +teardown() { + restore_dispatch_curl_stub +} + +@test "build_dispatch_json: sets event_type and includes non-empty fields" { + run build_dispatch_json "add-submodules" \ + version "boost-1.90.0" submodules "a, b" lang_codes "" + [ "$status" -eq 0 ] + echo "$output" | jq -e '.event_type == "add-submodules"' >/dev/null + echo "$output" | jq -e '.client_payload.version == "boost-1.90.0"' >/dev/null + echo "$output" | jq -e '.client_payload.submodules == "a, b"' >/dev/null + echo "$output" | jq -e '.client_payload | has("lang_codes") | not' >/dev/null +} + +@test "build_dispatch_json: omits all-empty client_payload keys" { + run build_dispatch_json "start-translation" version "" lang_codes "" extensions "" + [ "$status" -eq 0 ] + echo "$output" | jq -e '.event_type == "start-translation"' >/dev/null + echo "$output" | jq -e '.client_payload == {}' >/dev/null +} + +@test "resolve_trigger_repo: explicit repo wins" { + run resolve_trigger_repo "explicit/owner" + [ "$status" -eq 0 ] + [ "$output" = "explicit/owner" ] +} + +@test "resolve_trigger_repo: falls back to GITHUB_REPOSITORY" { + run bash -c ' + source "'"$ROOT"'/scripts/trigger-dispatch-common.sh" + export GITHUB_REPOSITORY="envorg/envrepo" + resolve_trigger_repo "" + ' + [ "$status" -eq 0 ] + [ "$output" = "envorg/envrepo" ] +} + +@test "resolve_trigger_repo: falls back to DEFAULT_REPO" { + run bash -c ' + cd "$(mktemp -d)" + source "'"$ROOT"'/scripts/trigger-dispatch-common.sh" + unset GITHUB_REPOSITORY + resolve_trigger_repo "" + ' + [ "$status" -eq 0 ] + [ "$output" = "cppalliance/boost-docs-translation" ] +} + +@test "resolve_trigger_repo: returns 1 when all sources unset" { + run bash -c ' + cd "$(mktemp -d)" + source "'"$ROOT"'/scripts/trigger-dispatch-common.sh" + unset GITHUB_REPOSITORY + DEFAULT_REPO="" + resolve_trigger_repo "" + ' + [ "$status" -eq 1 ] +} + +@test "resolve_trigger_token: explicit token" { + run resolve_trigger_token "explicit-token" + [ "$status" -eq 0 ] + [ "$output" = "explicit-token" ] +} + +@test "resolve_trigger_token: falls back to GH_TOKEN" { + run bash -c ' + source "'"$ROOT"'/scripts/trigger-dispatch-common.sh" + export GH_TOKEN="from-env" + unset GITHUB_TOKEN + resolve_trigger_token "" + ' + [ "$status" -eq 0 ] + [ "$output" = "from-env" ] +} + +@test "resolve_trigger_token: returns 1 when missing" { + run bash -c ' + source "'"$ROOT"'/scripts/trigger-dispatch-common.sh" + unset GH_TOKEN GITHUB_TOKEN + resolve_trigger_token "" + ' + [ "$status" -eq 1 ] + [[ "$output" == *"error: set GH_TOKEN"* ]] +} + +@test "post_repository_dispatch: succeeds on HTTP 204" { + install_dispatch_curl_stub + run post_repository_dispatch "owner/repo" "fake-token" \ + '{"event_type":"add-submodules","client_payload":{}}' "add-submodules" + [ "$status" -eq 0 ] + [[ "$output" == *"Dispatched add-submodules to owner/repo (HTTP 204)"* ]] + [ -f "$MOCK_DISPATCH_REQUEST_LOG" ] + grep -q 'api.github.com/repos/owner/repo/dispatches' "$MOCK_DISPATCH_REQUEST_LOG" +} + +@test "post_repository_dispatch: fails on non-204" { + install_dispatch_curl_stub + MOCK_DISPATCH_STATUS=403 + MOCK_DISPATCH_RESPONSE_BODY='{"message":"Forbidden"}' + export MOCK_DISPATCH_STATUS MOCK_DISPATCH_RESPONSE_BODY + run post_repository_dispatch "owner/repo" "fake-token" \ + '{"event_type":"add-submodules","client_payload":{}}' "add-submodules" + [ "$status" -eq 1 ] + [[ "$output" == *"GitHub API error: HTTP 403"* ]] +} diff --git a/tests/test_trigger_start_translation.bats b/tests/test_trigger_start_translation.bats new file mode 100644 index 0000000..2257d29 --- /dev/null +++ b/tests/test_trigger_start_translation.bats @@ -0,0 +1,55 @@ +#!/usr/bin/env bats + +setup() { + ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + SCRIPT="$ROOT/scripts/trigger-start-translation.sh" + # shellcheck source=tests/helpers/http_mock.bash + source "$BATS_TEST_DIRNAME/helpers/http_mock.bash" +} + +teardown() { + restore_dispatch_curl_stub +} + +@test "trigger-start-translation: --help exits 0" { + run "$SCRIPT" --help + [ "$status" -eq 0 ] + [[ "$output" == *"trigger-start-translation.sh"* ]] +} + +@test "trigger-start-translation: unknown option errors with usage" { + run "$SCRIPT" --not-a-flag + [ "$status" -eq 1 ] + [[ "$output" == *"Unknown option: --not-a-flag"* ]] + [[ "$output" == *"Usage:"* ]] +} + +@test "trigger-start-translation: minimal dispatch includes defaults and omits lang_codes" { + install_dispatch_curl_stub + run "$SCRIPT" --repo owner/repo --token fake-token + [ "$status" -eq 0 ] + [[ "$output" == *"Dispatched start-translation to owner/repo (HTTP 204)"* ]] + body="$(extract_dispatch_request_body "$MOCK_DISPATCH_REQUEST_LOG")" + echo "$body" | jq -e '.event_type == "start-translation"' >/dev/null + echo "$body" | jq -e '.client_payload.version == "boost-1.90.0"' >/dev/null + echo "$body" | jq -e '.client_payload.extensions == ".adoc, .qbk"' >/dev/null + echo "$body" | jq -e '.client_payload | has("lang_codes") | not' >/dev/null +} + +@test "trigger-start-translation: explicit --lang-codes included in payload" { + install_dispatch_curl_stub + run "$SCRIPT" --repo owner/repo --token fake-token --lang-codes zh_Hans + [ "$status" -eq 0 ] + body="$(extract_dispatch_request_body "$MOCK_DISPATCH_REQUEST_LOG")" + echo "$body" | jq -e '.client_payload.lang_codes == "zh_Hans"' >/dev/null +} + +@test "trigger-start-translation: non-204 from GitHub API fails" { + install_dispatch_curl_stub + MOCK_DISPATCH_STATUS=500 + MOCK_DISPATCH_RESPONSE_BODY='{"message":"Server Error"}' + export MOCK_DISPATCH_STATUS MOCK_DISPATCH_RESPONSE_BODY + run "$SCRIPT" --repo owner/repo --token fake-token + [ "$status" -eq 1 ] + [[ "$output" == *"GitHub API error: HTTP 500"* ]] +} From 346ea95afa8edabf5c9dc1db3737ae551c1832cf Mon Sep 17 00:00:00 2001 From: whisper67265 Date: Thu, 30 Jul 2026 15:17:18 -0600 Subject: [PATCH 2/3] fix the coderabbitai review --- scripts/trigger-add-submodules.sh | 4 ++++ tests/test_trigger_add_submodules.bats | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/scripts/trigger-add-submodules.sh b/scripts/trigger-add-submodules.sh index c7f9882..2e924b5 100755 --- a/scripts/trigger-add-submodules.sh +++ b/scripts/trigger-add-submodules.sh @@ -81,6 +81,10 @@ while [[ $# -gt 0 ]]; do esac done +# Trim leading/trailing whitespace; reject empty or whitespace-only values. +SUBMODULES="${SUBMODULES#"${SUBMODULES%%[![:space:]]*}"}" +SUBMODULES="${SUBMODULES%"${SUBMODULES##*[![:space:]]}"}" + if [[ -z "$SUBMODULES" ]]; then echo "error: --submodules is required (comma-separated library names)" >&2 usage >&2 diff --git a/tests/test_trigger_add_submodules.bats b/tests/test_trigger_add_submodules.bats index 6695ce2..a61c06e 100644 --- a/tests/test_trigger_add_submodules.bats +++ b/tests/test_trigger_add_submodules.bats @@ -31,6 +31,13 @@ teardown() { [[ "$output" == *"Usage:"* ]] } +@test "trigger-add-submodules: whitespace-only --submodules errors with usage" { + run "$SCRIPT" --repo owner/repo --submodules ' ' + [ "$status" -eq 1 ] + [[ "$output" == *"error: --submodules is required"* ]] + [[ "$output" == *"Usage:"* ]] +} + @test "trigger-add-submodules: explicit --submodules dispatches payload" { install_dispatch_curl_stub run "$SCRIPT" --repo owner/repo --token fake-token --submodules 'a, b' From 3c4134114e7c9cb9c9da966e80b2be6e86c35a3f Mon Sep 17 00:00:00 2001 From: whisper67265 Date: Fri, 31 Jul 2026 02:25:13 -0600 Subject: [PATCH 3/3] fix the first reviewer comments --- scripts/trigger-add-submodules.sh | 14 ++-- scripts/trigger-dispatch-common.sh | 25 +++---- tests/helpers/http_mock.bash | 89 ++++++++++++++--------- tests/test_trigger_add_submodules.bats | 6 +- tests/test_trigger_dispatch_common.bats | 12 +-- tests/test_trigger_start_translation.bats | 6 +- 6 files changed, 85 insertions(+), 67 deletions(-) diff --git a/scripts/trigger-add-submodules.sh b/scripts/trigger-add-submodules.sh index 2e924b5..a63b441 100755 --- a/scripts/trigger-add-submodules.sh +++ b/scripts/trigger-add-submodules.sh @@ -60,18 +60,22 @@ VERSION="" SUBMODULES="" LANG_CODES="" +shift_pair_into() { + printf -v "$1" '%s' "${2:-}" +} + while [[ $# -gt 0 ]]; do case "$1" in --repo) - REPO="${2:-}"; shift 2 || exit 1 ;; + shift_pair_into REPO "$2"; shift 2 || exit 1 ;; --token) - TOKEN="${2:-}"; shift 2 || exit 1 ;; + shift_pair_into TOKEN "$2"; shift 2 || exit 1 ;; --version) - VERSION="${2:-}"; shift 2 || exit 1 ;; + shift_pair_into VERSION "$2"; shift 2 || exit 1 ;; --submodules) - SUBMODULES="${2:-}"; shift 2 || exit 1 ;; + shift_pair_into SUBMODULES "$2"; shift 2 || exit 1 ;; --lang-codes) - LANG_CODES="${2:-}"; shift 2 || exit 1 ;; + shift_pair_into LANG_CODES "$2"; shift 2 || exit 1 ;; -h|--help) usage; exit 0 ;; *) diff --git a/scripts/trigger-dispatch-common.sh b/scripts/trigger-dispatch-common.sh index 679a47f..7d7a017 100644 --- a/scripts/trigger-dispatch-common.sh +++ b/scripts/trigger-dispatch-common.sh @@ -67,25 +67,20 @@ build_dispatch_json() { local event_type="$1" shift if command -v jq >/dev/null 2>&1; then - local json_pairs='[]' key val - while [[ $# -gt 0 ]]; do - key="$1" - val="$2" - shift 2 - json_pairs="$(jq -n --argjson arr "$json_pairs" --arg k "$key" --arg v "$val" \ - '$arr + [{key: $k, value: $v}]')" - done - jq -n --arg event_type "$event_type" --argjson kv_pairs "$json_pairs" \ + jq -n --arg event_type "$event_type" --args \ '{ event_type: $event_type, client_payload: ( - {} - | reduce $kv_pairs[] as $p ( - .; - if ($p.value | length) > 0 then . + {($p.key): $p.value} else . end - ) + reduce range(0; $ARGS.positional | length; 2) as $i ( + {}; + if ($ARGS.positional[$i + 1] | length) > 0 + then . + {($ARGS.positional[$i]): $ARGS.positional[$i + 1]} + else . + end + ) ) - }' + }' \ + "$@" return 0 fi local py="" diff --git a/tests/helpers/http_mock.bash b/tests/helpers/http_mock.bash index 1ccc4e8..72789ef 100644 --- a/tests/helpers/http_mock.bash +++ b/tests/helpers/http_mock.bash @@ -9,6 +9,17 @@ MOCK_WEBLATE_REQUEST_LOG="" MOCK_WEBLATE_SERVER_SCRIPT="" _CURL_ORIG_PATH="" +common_setup() { + local root_dir + root_dir="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + # shellcheck disable=SC2034 + export ROOT="$root_dir" +} + +common_teardown() { + restore_dispatch_curl_stub +} + start_weblate_mock_server() { local status="${MOCK_WEBLATE_STATUS:-202}" local body="${MOCK_WEBLATE_BODY:-{\"status\":\"accepted\"}}" @@ -64,7 +75,7 @@ PYEOF python3 "$MOCK_WEBLATE_SERVER_SCRIPT" & MOCK_WEBLATE_PID=$! - local i=0 port="" + local i=0 port="" sleep_sec=0.05 while [[ -z "$port" ]]; do if [[ -s "$MOCK_WEBLATE_PORT_FILE" ]]; then port="$(<"$MOCK_WEBLATE_PORT_FILE")" @@ -84,7 +95,8 @@ PYEOF echo "http_mock: mock server failed to start" >&2 return 1 fi - sleep 0.05 + sleep "$sleep_sec" + sleep_sec="$(awk "BEGIN {v=$sleep_sec*2; print (v>0.5?0.5:v)}")" done MOCK_WEBLATE_PORT="$port" @@ -109,18 +121,20 @@ stop_weblate_mock_server() { unset MOCK_WEBLATE_STATUS MOCK_WEBLATE_BODY MOCK_WEBLATE_DELAY_SEC } -install_curl_timeout_stub() { - install_curl_stub -} - -install_curl_stub() { +_init_curl_wrapper_dir() { _CURL_ORIG_PATH="$PATH" - local wrapper_dir - wrapper_dir="$(mktemp -d)" + CURL_WRAPPER_DIR="$(mktemp -d)" REAL_CURL="$(command -v curl)" - export REAL_CURL - cat >"$wrapper_dir/curl" <<'EOF' -#!/usr/bin/env bash + export REAL_CURL CURL_WRAPPER_DIR +} + +_activate_curl_wrapper() { + chmod +x "$CURL_WRAPPER_DIR/curl" + export PATH="$CURL_WRAPPER_DIR:$PATH" +} + +_curl_stub_preamble() { + cat <<'STUB_EOF' if [[ -n "${MOCK_CURL_EXIT:-}" ]]; then echo "mock curl: simulated exit ${MOCK_CURL_EXIT}" >&2 exit "$MOCK_CURL_EXIT" @@ -129,11 +143,27 @@ if [[ "${MOCK_CURL_TIMEOUT:-}" == "1" ]]; then echo "mock curl: simulated timeout" >&2 exit 28 fi +STUB_EOF +} + +_curl_stub_exec_line() { + cat <<'STUB_EOF' exec "$REAL_CURL" "$@" -EOF - chmod +x "$wrapper_dir/curl" - CURL_WRAPPER_DIR="$wrapper_dir" - export PATH="$wrapper_dir:$PATH" +STUB_EOF +} + +install_curl_timeout_stub() { + install_curl_stub +} + +install_curl_stub() { + _init_curl_wrapper_dir + { + echo '#!/usr/bin/env bash' + _curl_stub_preamble + _curl_stub_exec_line + } >"$CURL_WRAPPER_DIR/curl" + _activate_curl_wrapper } restore_curl_stub() { @@ -148,27 +178,17 @@ restore_curl_stub() { # Intercept GitHub repository_dispatch POST (no real network). install_dispatch_curl_stub() { - _CURL_ORIG_PATH="$PATH" - local wrapper_dir - wrapper_dir="$(mktemp -d)" - REAL_CURL="$(command -v curl)" - export REAL_CURL + _init_curl_wrapper_dir MOCK_DISPATCH_REQUEST_LOG="${MOCK_DISPATCH_REQUEST_LOG:-$(mktemp)}" export MOCK_DISPATCH_REQUEST_LOG MOCK_DISPATCH_STATUS="${MOCK_DISPATCH_STATUS:-204}" export MOCK_DISPATCH_STATUS MOCK_DISPATCH_RESPONSE_BODY="${MOCK_DISPATCH_RESPONSE_BODY:-}" export MOCK_DISPATCH_RESPONSE_BODY - cat >"$wrapper_dir/curl" <<'EOF' -#!/usr/bin/env bash -if [[ -n "${MOCK_CURL_EXIT:-}" ]]; then - echo "mock curl: simulated exit ${MOCK_CURL_EXIT}" >&2 - exit "$MOCK_CURL_EXIT" -fi -if [[ "${MOCK_CURL_TIMEOUT:-}" == "1" ]]; then - echo "mock curl: simulated timeout" >&2 - exit 28 -fi + { + echo '#!/usr/bin/env bash' + _curl_stub_preamble + cat <<'EOF' args=("$@") url="" method="" @@ -233,15 +253,14 @@ if [[ "$url" == *"api.github.com/repos/"*/dispatches && "$method" == "POST" ]]; fi fi if [[ -n "$write_out" ]]; then - printf '%s' "${MOCK_DISPATCH_STATUS:-204}" + printf '%s' "$MOCK_DISPATCH_STATUS" fi exit 0 fi exec "$REAL_CURL" "$@" EOF - chmod +x "$wrapper_dir/curl" - CURL_WRAPPER_DIR="$wrapper_dir" - export PATH="$wrapper_dir:$PATH" + } >"$CURL_WRAPPER_DIR/curl" + _activate_curl_wrapper } restore_dispatch_curl_stub() { diff --git a/tests/test_trigger_add_submodules.bats b/tests/test_trigger_add_submodules.bats index a61c06e..e4601eb 100644 --- a/tests/test_trigger_add_submodules.bats +++ b/tests/test_trigger_add_submodules.bats @@ -1,14 +1,14 @@ #!/usr/bin/env bats setup() { - ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" - SCRIPT="$ROOT/scripts/trigger-add-submodules.sh" # shellcheck source=tests/helpers/http_mock.bash source "$BATS_TEST_DIRNAME/helpers/http_mock.bash" + common_setup + SCRIPT="$ROOT/scripts/trigger-add-submodules.sh" } teardown() { - restore_dispatch_curl_stub + common_teardown } @test "trigger-add-submodules: --help exits 0" { diff --git a/tests/test_trigger_dispatch_common.bats b/tests/test_trigger_dispatch_common.bats index 947106a..53130c4 100644 --- a/tests/test_trigger_dispatch_common.bats +++ b/tests/test_trigger_dispatch_common.bats @@ -1,15 +1,15 @@ #!/usr/bin/env bats setup() { - ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" # shellcheck source=tests/helpers/http_mock.bash source "$BATS_TEST_DIRNAME/helpers/http_mock.bash" + common_setup # shellcheck source=/dev/null source "$ROOT/scripts/trigger-dispatch-common.sh" } teardown() { - restore_dispatch_curl_stub + common_teardown } @test "build_dispatch_json: sets event_type and includes non-empty fields" { @@ -96,8 +96,8 @@ teardown() { @test "post_repository_dispatch: succeeds on HTTP 204" { install_dispatch_curl_stub - run post_repository_dispatch "owner/repo" "fake-token" \ - '{"event_type":"add-submodules","client_payload":{}}' "add-submodules" + local payload='{"event_type":"add-submodules","client_payload":{}}' + run post_repository_dispatch "owner/repo" "fake-token" "$payload" "add-submodules" [ "$status" -eq 0 ] [[ "$output" == *"Dispatched add-submodules to owner/repo (HTTP 204)"* ]] [ -f "$MOCK_DISPATCH_REQUEST_LOG" ] @@ -106,11 +106,11 @@ teardown() { @test "post_repository_dispatch: fails on non-204" { install_dispatch_curl_stub + local payload='{"event_type":"add-submodules","client_payload":{}}' MOCK_DISPATCH_STATUS=403 MOCK_DISPATCH_RESPONSE_BODY='{"message":"Forbidden"}' export MOCK_DISPATCH_STATUS MOCK_DISPATCH_RESPONSE_BODY - run post_repository_dispatch "owner/repo" "fake-token" \ - '{"event_type":"add-submodules","client_payload":{}}' "add-submodules" + run post_repository_dispatch "owner/repo" "fake-token" "$payload" "add-submodules" [ "$status" -eq 1 ] [[ "$output" == *"GitHub API error: HTTP 403"* ]] } diff --git a/tests/test_trigger_start_translation.bats b/tests/test_trigger_start_translation.bats index 2257d29..c9bc44b 100644 --- a/tests/test_trigger_start_translation.bats +++ b/tests/test_trigger_start_translation.bats @@ -1,14 +1,14 @@ #!/usr/bin/env bats setup() { - ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" - SCRIPT="$ROOT/scripts/trigger-start-translation.sh" # shellcheck source=tests/helpers/http_mock.bash source "$BATS_TEST_DIRNAME/helpers/http_mock.bash" + common_setup + SCRIPT="$ROOT/scripts/trigger-start-translation.sh" } teardown() { - restore_dispatch_curl_stub + common_teardown } @test "trigger-start-translation: --help exits 0" {