From 9ff8ca383dbaa6e3313884c03a619a97291b5595 Mon Sep 17 00:00:00 2001 From: pavel-te Date: Thu, 6 Aug 2026 10:19:14 +0300 Subject: [PATCH 1/2] [ci18-7398] Make the docs runnable: kill the QA note, fix the examples (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three surfaces claimed things that were not true, and the one command we recommend first was gated behind a note telling the reader it would not work. **The QA note was false.** README and the CLI both said detect_config lives on the QA environment and that --api-url must be pointed there. Checked against production: POST https://app.ptc.wpml.org/api/v1/detect_config returns 200 with a real body, anonymously, no token. `ptc init` completes against the default URL. Removed from the README, from `show_init_help` and from the 404 hint, which now says what a 404 there actually means. While there: the quick start passed PTC_API_TOKEN to `init`, which does not need one — detect_config is anonymous, and the CLI itself logs that. **A README example failed on copy-paste.** `--file-tag-name=feature-branch` exits 1 with "Unknown option". The `=` form is implemented for --api-url, --api-token, --monitor-interval, --monitor-max-attempts and --action only, and README:85 uses a working --api-url=..., so the rule cannot be inferred. Fixed the example and wrote the rule down under Main Options. Widening the parser is the other way to resolve this; it changes CLI behaviour and needs a release, so it is not in a documentation fix. **Both CI blocks were stale.** The GitHub one pinned v1.0.0, used actions/checkout@v4 and peter-evans/create-pull-request@v5, and opened a fresh PR per run; the GitLab one never downloaded the CLI and triggered on merge_requests, which is not loop-safe. Both are now what `ptc init` prints, verified by running it. The version-pinning example moved v1.0.0 -> v1.0.3. **Dependencies.** The generated GitLab job installed `jq`, which appears nowhere in this script, and omitted `unzip`, which unpacks every download. Checked alpine:3.22: unzip is present as a busybox applet and extraction works, so the recipe was not broken — but it is named now so it survives an image swap. git stays: the push step needs it, the CLI does not. **Three of five examples did not parse.** java-app, react-app and wordpress-wpsite were KEY=VALUE files using {lang}; each died with "Missing 'files:' section", and only {{lang}} is ever substituted. Rewritten as YAML the parser accepts, renamed .config -> .yml since the extension is what suggested the wrong format. config.local.yml.example taught api_token:, deprecated and ignored since v1.0.x — it now shows the env var instead. **Why nothing caught this:** no test ever fed an example to the parser. Added tests/test-config-examples.sh, which builds a scratch project from each config and runs a real --dry-run. Confirmed it fails on the old files before trusting it: dropping the previous java-app.config back in turns it red on both the format and the placeholder. Suite: 230 assertions across 7 files, 0 failures. `bash -n` clean. Every fenced block in the README parses as what it is labelled (four Troubleshooting blocks showed CLI output while tagged bash; they are `text` now), and every link resolves. The wiki half of this ticket is pushed separately as 3d3282d. Co-authored-by: Pavel Tkachenko Co-authored-by: Claude Opus 5 (1M context) --- README.md | 148 ++++++++++++----------- config/examples/config.local.yml.example | 18 +-- config/examples/java-app.config | 8 -- config/examples/java-app.yml | 15 +++ config/examples/react-app.config | 8 -- config/examples/react-app.yml | 15 +++ config/examples/wordpress-wpsite.config | 19 --- config/examples/wordpress-wpsite.yml | 23 ++++ docs/DEVELOPMENT.md | 10 +- ptc-cli.sh | 12 +- tests/test-config-examples.sh | 121 ++++++++++++++++++ 11 files changed, 275 insertions(+), 122 deletions(-) delete mode 100644 config/examples/java-app.config create mode 100644 config/examples/java-app.yml delete mode 100644 config/examples/react-app.config create mode 100644 config/examples/react-app.yml delete mode 100644 config/examples/wordpress-wpsite.config create mode 100644 config/examples/wordpress-wpsite.yml create mode 100755 tests/test-config-examples.sh diff --git a/README.md b/README.md index 537a908..76a7035 100644 --- a/README.md +++ b/README.md @@ -34,16 +34,14 @@ you what it found, and writes a ready-to-use config plus a CI snippet. ./ptc-cli.sh init --dry-run --verbose # Non-interactive (CI), overwrite an existing config -PTC_API_TOKEN=your-token ./ptc-cli.sh init --yes --force +./ptc-cli.sh init --yes --force ``` -The token is read from `PTC_API_TOKEN` (env-first) or `--api-token`. If the -project layout isn't recognised, `init` writes a commented template you can fill -in — it never hard-fails. Ignore extra paths by listing gitignore-style patterns -in a `.ptcignore` file at the repo root. - -> **Note:** `detect_config` is currently on the QA environment. Until it reaches -> production, point `--api-url` at the QA host. +`init` needs no API token — `detect_config` is anonymous. A token is required +only later, to upload and translate. If the project layout isn't recognised, +`init` writes a commented template you can fill in — it never hard-fails. Ignore +extra paths by listing gitignore-style patterns in a `.ptcignore` file at the +repo root. ### Using a hand-written Configuration File @@ -114,6 +112,12 @@ chmod +x ptc-cli.sh - `-h, --help` - Show help - `--version` - Show version +**Writing option values.** Every option above takes its value as a separate +argument (`--file-tag-name my-branch`). The `--flag=value` form works only for +`--api-url`, `--api-token`, `--monitor-interval`, `--monitor-max-attempts` and +`--action`; anywhere else it is reported as an unknown option. (`init` accepts +`=` for `--api-url`, `--api-token` and `--project-dir`.) + ### Authentication Provide your API token through the `PTC_API_TOKEN` environment variable: @@ -197,8 +201,8 @@ files: # Dry run to see what would happen ./ptc-cli.sh -c config.yml --dry-run --verbose -# Override specific settings -./ptc-cli.sh -c config.yml --file-tag-name=feature-branch +# Override specific settings (short flags take a separate argument, not --flag=value) +./ptc-cli.sh -c config.yml --file-tag-name feature-branch # Isolated actions ./ptc-cli.sh -c config.yml --action upload # Only upload files @@ -307,12 +311,12 @@ Pipelines download the script from a **pinned release tag**, not a moving branch so a push to `main` can never change what your build runs: ```bash -curl -fsSL https://raw.githubusercontent.com/OnTheGoSystems/ptc-cli/v1.0.0/ptc-cli.sh -o ptc-cli.sh +curl -fsSL https://raw.githubusercontent.com/OnTheGoSystems/ptc-cli/v1.0.3/ptc-cli.sh -o ptc-cli.sh ``` -Use `v1.0.0` to pin an exact release, or the floating `v1` tag to pick up -backward-compatible updates automatically. The `ptc init` command scaffolds the -pinned URL for you. +Use an exact release tag such as `v1.0.3` to pin, or the floating `v1` tag to +pick up backward-compatible updates automatically. `ptc init` scaffolds the +pinned URL for you, at the version of the CLI that printed it. **Verify the download** against the SHA256 checksum published on the [release page](https://github.com/OnTheGoSystems/ptc-cli/releases): @@ -332,61 +336,34 @@ the server can attribute traffic to a specific release. Add PTC_API_TOKEN to the repository secrets (Settings -> Secrets and variables -> Actions -> New repository secret). Ensure to turn on the "Allow GitHub Actions to create and approve pull requests" permission in the repository settings (Settings -> Actions -> General -> Workflow permissions). +This is what `ptc init` writes for you. It runs the CLI through +[`ptc-action`](https://github.com/OnTheGoSystems/ptc-action), which vendors a +pinned copy of this script, so there is nothing to download at job time: + ```yaml -name: Process Translation Files +name: PTC Translations on: - workflow_dispatch: # Manual trigger + push: + branches: [main] + workflow_dispatch: {} + +permissions: + contents: write + pull-requests: write jobs: - process-translations: + translate: runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup PTC CLI - run: | - curl -fsSL https://raw.githubusercontent.com/OnTheGoSystems/ptc-cli/v1.0.0/ptc-cli.sh -o ptc-cli.sh - chmod +x ptc-cli.sh - - - name: Process translations with PTC CLI - env: - PTC_API_TOKEN: ${{ secrets.PTC_API_TOKEN }} - run: | - ./ptc-cli.sh \ - --config-file .ptc/config.yml \ - --verbose - - - name: Clean up temporary files - run: | - rm -f ptc-cli.sh - - - name: Create Pull Request with translations - if: success() - uses: peter-evans/create-pull-request@v5 + - uses: actions/checkout@v7 + - uses: OnTheGoSystems/ptc-action@v1 with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: "🌐 Update translations via PTC CLI" - title: "🌐 Update translations from PTC" - body: | - ## 🌐 Translation Update - - This PR contains new translations processed by PTC CLI. - - **Triggered by:** ${{ github.event_name }} - **Branch:** ${{ github.ref_name }} - **Commit:** ${{ github.sha }} - - --- - *Auto-generated by GitHub Actions* - delete-branch: true + api-token: ${{ secrets.PTC_API_TOKEN }} + config-file: .ptc-config.yml + create-pr: true ``` -Sample `.ptc/config.yml` file: +Sample `.ptc-config.yml` file: ```yaml source_locale: en @@ -399,19 +376,46 @@ files: ### GitLab CI +There is no GitLab component to include: `include: component:` is resolved by +your own GitLab instance, so a component published anywhere else is unreachable. +`ptc init` prints this self-contained job instead: + ```yaml -process_translations: +ptc-translate: stage: deploy + image: alpine:3.22 + rules: + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' + before_script: + - apk add --no-cache bash curl git unzip script: + - curl -fsSL https://raw.githubusercontent.com/OnTheGoSystems/ptc-cli/v1.0.3/ptc-cli.sh -o ptc-cli.sh - chmod +x ptc-cli.sh - - ./ptc-cli.sh -c config.yml -v - variables: - PTC_API_TOKEN: "$CI_PTC_API_TOKEN" - only: - - merge_requests - - main + - ./ptc-cli.sh --config-file .ptc-config.yml + - | + if ! git diff --quiet; then + git config user.email "ci@ptc" + git config user.name "PTC Translate" + git checkout -B ptc/translations + git add -A + git commit -m "chore(i18n): update translations via PTC [skip ci]" + git push -o merge_request.create \ + -o merge_request.target="$CI_DEFAULT_BRANCH" \ + -o merge_request.title="Update translations from PTC" \ + -f "https://gitlab-ci-token:${PTC_GIT_PUSH_TOKEN:-$CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" HEAD:ptc/translations + fi ``` +Store `PTC_API_TOKEN` as a **masked** CI/CD variable. The push needs a token +that may write to the repository: `CI_JOB_TOKEN` can, but only if a maintainer +enables Settings -> CI/CD -> Job token permissions -> "Allow Git push requests +to the repository" (GitLab 18.4+, off by default). Otherwise set +`PTC_GIT_PUSH_TOKEN` to a project access token with the `write_repository` +scope, also masked. + +Loop-safe twice over: the job runs only on a push to the default branch, and the +translation push targets `ptc/translations`, which cannot re-trigger it. + ### Additional Translation Files When using YAML configuration, you can specify additional files to be generated (useful for WordPress): @@ -434,14 +438,14 @@ files: **Common Issues:** 1. **HTTP 401 Unauthorized** - ```bash + ```text [ERROR] Failed to upload file: example.json (HTTP 401) ``` - Check your API token - Verify token has correct permissions 2. **HTTP 403 Forbidden** - ```bash + ```text [ERROR] Failed to upload file: example.json (HTTP 403) ``` - Check your API token @@ -449,15 +453,15 @@ files: - Verify token has correct permissions 3. **Files not found** - ```bash + ```text [ERROR] No files found for pattern: {{lang}}.json ``` - Check file paths in config - Verify source locale matches your files - Use `--verbose` to see search details -5. **Translation timeout** - ```bash +4. **Translation timeout** + ```text [WARNING] Timed out files: 1 ``` - Increase `--monitor-max-attempts` diff --git a/config/examples/config.local.yml.example b/config/examples/config.local.yml.example index 5d19979..d89614c 100644 --- a/config/examples/config.local.yml.example +++ b/config/examples/config.local.yml.example @@ -1,12 +1,14 @@ -# Example local configuration file with API token -# Copy this file to config.local.yml and fill in your actual API token -# This file is ignored by git for security +# Example of a local, uncommitted configuration file. +# Copy it to config.local.yml — git ignores that name — and adjust the paths. +# +# There is no api_token: key here on purpose. It is deprecated and ignored (the +# CLI warns and carries on), because a token written into a file gets committed +# sooner or later. Pass it through the environment instead. # Basic settings source_locale: en file_tag_name: main api_url: https://app.ptc.wpml.org/api/v1/ -api_token: your-actual-secret-api-token-here # Monitoring settings monitor_interval: 5 @@ -16,13 +18,13 @@ monitor_max_attempts: 100 files: - file: src/locales/en.json output: src/locales/{{lang}}.json - + - file: admin/locales/en.json output: admin/locales/{{lang}}.json # Usage: # 1. Copy this file: cp config/examples/config.local.yml.example config.local.yml -# 2. Edit config.local.yml with your real API token -# 3. Run: ./ptc-cli.sh --config-file config.local.yml -# +# 2. Edit config.local.yml — paths only, never a token +# 3. Run: PTC_API_TOKEN=your-token ./ptc-cli.sh --config-file config.local.yml +# # Note: config.local.yml is automatically ignored by git diff --git a/config/examples/java-app.config b/config/examples/java-app.config deleted file mode 100644 index c0f7685..0000000 --- a/config/examples/java-app.config +++ /dev/null @@ -1,8 +0,0 @@ -# Configuration for Java application -# File: config/examples/java-app.config - -SOURCE_LOCALE=en -PATTERNS='src/main/resources/i18n/messages_{lang}.properties,src/main/resources/i18n/errors_{lang}.properties' -PROJECT_DIR=. -FILE_TAG_NAME=develop -VERBOSE=false diff --git a/config/examples/java-app.yml b/config/examples/java-app.yml new file mode 100644 index 0000000..ba6500d --- /dev/null +++ b/config/examples/java-app.yml @@ -0,0 +1,15 @@ +# Configuration for a Java application +# Usage: ./ptc-cli.sh --config-file config/examples/java-app.yml +# +# The token is never written here — provide it via the PTC_API_TOKEN +# environment variable. + +source_locale: en +file_tag_name: develop + +files: + - file: src/main/resources/i18n/messages_en.properties + output: src/main/resources/i18n/messages_{{lang}}.properties + + - file: src/main/resources/i18n/errors_en.properties + output: src/main/resources/i18n/errors_{{lang}}.properties diff --git a/config/examples/react-app.config b/config/examples/react-app.config deleted file mode 100644 index 0b0e30c..0000000 --- a/config/examples/react-app.config +++ /dev/null @@ -1,8 +0,0 @@ -# Configuration for React application -# File: config/examples/react-app.config - -SOURCE_LOCALE=en -PATTERNS='public/locales/{lang}/common.json,public/locales/{lang}/translation.json' -PROJECT_DIR=. -FILE_TAG_NAME=main -VERBOSE=true diff --git a/config/examples/react-app.yml b/config/examples/react-app.yml new file mode 100644 index 0000000..0c25982 --- /dev/null +++ b/config/examples/react-app.yml @@ -0,0 +1,15 @@ +# Configuration for a React application +# Usage: ./ptc-cli.sh --config-file config/examples/react-app.yml +# +# The token is never written here — provide it via the PTC_API_TOKEN +# environment variable. + +source_locale: en +file_tag_name: main + +files: + - file: public/locales/en/common.json + output: public/locales/{{lang}}/common.json + + - file: public/locales/en/translation.json + output: public/locales/{{lang}}/translation.json diff --git a/config/examples/wordpress-wpsite.config b/config/examples/wordpress-wpsite.config deleted file mode 100644 index e0a6af9..0000000 --- a/config/examples/wordpress-wpsite.config +++ /dev/null @@ -1,19 +0,0 @@ -# Configuration for WordPress WPSite project -# File: config/examples/wordpress-wpsite.config - -SOURCE_LOCALE=en -PATTERNS='languages/wpsite.pot' -PROJECT_DIR=. -FILE_TAG_NAME=main -VERBOSE=true - -# WordPress specific settings -# Common language codes for WordPress: -# en_US (English), de_DE (German), fr_FR (French), es_ES (Spanish), -# it_IT (Italian), pt_BR (Portuguese Brazil), ru_RU (Russian) - -# Example patterns for different WordPress structures: -# Plugin translations: 'languages/plugins/plugin-name/plugin-name-{lang}.po' -# Theme translations: 'languages/themes/theme-name/{lang}.po' -# Core translations: 'languages/{lang}.po' -# WooCommerce: 'languages/plugins/woocommerce/woocommerce-{lang}.po' diff --git a/config/examples/wordpress-wpsite.yml b/config/examples/wordpress-wpsite.yml new file mode 100644 index 0000000..823ad96 --- /dev/null +++ b/config/examples/wordpress-wpsite.yml @@ -0,0 +1,23 @@ +# Configuration for a WordPress site or plugin +# Usage: ./ptc-cli.sh --config-file config/examples/wordpress-wpsite.yml +# +# The token is never written here — provide it via the PTC_API_TOKEN +# environment variable. +# +# WordPress expects locale codes like de_DE or fr_FR rather than bare de / fr. +# Whatever {{lang}} expands to comes from the locales configured on the PTC +# project, so set those up to match the names WordPress loads. + +source_locale: en +file_tag_name: main + +files: + - file: languages/wpsite.pot + output: languages/wpsite-{{lang}}.po + # WordPress loads .mo at runtime and .json for block editor strings, so ask + # PTC to emit them alongside the .po. + additional_translation_files: + - type: mo + path: languages/wpsite-{{lang}}.mo + - type: json + path: languages/wpsite-{{lang}}.json diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 3a7a024..e064cce 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -7,9 +7,12 @@ ptc-cli-bash/ ├── ptc-cli.sh # Main executable script ├── README.md # Main documentation ├── config/ # Configurations -│ └── examples/ # Configuration examples -│ ├── react-app.config # For React applications -│ └── java-app.config # For Java applications +│ └── examples/ # Configuration examples (all valid YAML) +│ ├── full-config.yml # Every supported option +│ ├── react-app.yml # For React applications +│ ├── java-app.yml # For Java applications +│ ├── wordpress-wpsite.yml # For WordPress sites and plugins +│ └── config.local.yml.example # Uncommitted local config ├── tests/ # Testing │ ├── test-runner.sh # Main test runner (CLI invocation tests) │ ├── test-status-handling.sh # Status parsing, polling and preflight @@ -17,6 +20,7 @@ ptc-cli-bash/ │ ├── test-exit-codes.sh # Exit codes the CLI reports to CI │ ├── test-error-shapes.sh # Both shapes of a rejected API response │ ├── test-rate-limit.sh # 429 backoff and failure descriptions +│ ├── test-config-examples.sh # Every config/examples/* fed to the parser │ └── fixtures/ # Test data (created automatically) └── docs/ # Documentation └── DEVELOPMENT.md # This guide diff --git a/ptc-cli.sh b/ptc-cli.sh index 351196c..9256a7f 100755 --- a/ptc-cli.sh +++ b/ptc-cli.sh @@ -2962,7 +2962,11 @@ ptc-translate: rules: - if: '\$CI_PIPELINE_SOURCE == "push" && \$CI_COMMIT_BRANCH == \$CI_DEFAULT_BRANCH' before_script: - - apk add --no-cache bash curl git jq + # jq is never invoked by the CLI. unzip is - it unpacks the downloaded + # translations; alpine already provides it as a busybox applet, so it is + # named here only to keep the job working if the image is ever changed. + # git is needed by the push step below, not by the CLI. + - apk add --no-cache bash curl git unzip script: - curl -fsSL https://raw.githubusercontent.com/OnTheGoSystems/ptc-cli/v${VERSION}/ptc-cli.sh -o ptc-cli.sh - chmod +x ptc-cli.sh @@ -3041,8 +3045,8 @@ OPTIONS: -h, --help Show this help NOTE: - detect_config is currently on the QA environment. Until it reaches - production, point --api-url at the QA host." + detect_config is anonymous — init needs no API token. A token is only + required later, to upload and translate." } # `ptc init` entry point. @@ -3131,7 +3135,7 @@ cmd_init() { ;; 404) log_error "detect_config is not available on this server (HTTP 404)." - log_info "This endpoint is currently on the QA environment; point --api-url at the QA host." + log_info "Check --api-url. The endpoint is live on the default host; a self-hosted instance may predate it." return 1 ;; ""|000) diff --git a/tests/test-config-examples.sh b/tests/test-config-examples.sh new file mode 100755 index 0000000..dde2155 --- /dev/null +++ b/tests/test-config-examples.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +# Tests for config/examples/* — the files we tell people to copy. +# +# ci18-7398: three of the five examples were written as KEY=VALUE and every one +# of them died with "Missing 'files:' section", while a fourth taught the +# deprecated api_token: key. Nothing caught it because no test ever fed an +# example to the parser. This does: each example is run through a real --dry-run +# against a scratch project built from the paths it declares. + +set -uo pipefail + +readonly TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly PROJECT_DIR="$(dirname "$TEST_DIR")" +readonly CLI="$PROJECT_DIR/ptc-cli.sh" +readonly EXAMPLES_DIR="$PROJECT_DIR/config/examples" + +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly NC='\033[0m' + +test_count=0 +passed_count=0 +failed_count=0 + +pass() { echo -e "${GREEN}[PASS]${NC} $*"; passed_count=$((passed_count + 1)); test_count=$((test_count + 1)); } +fail() { echo -e "${RED}[FAIL]${NC} $*"; failed_count=$((failed_count + 1)); test_count=$((test_count + 1)); } + +# Builds a scratch project holding every source file the config declares, then +# runs the CLI against it. Source paths are the `- file:` entries; `output:` is +# what PTC would write, so those are deliberately not created. +run_example() { + local config="$1" + local name workdir line src + name="$(basename "$config")" + + workdir="$(mktemp -d)" + trap 'rm -rf "$workdir"' RETURN + + while IFS= read -r line; do + src="${line#*file:}" + src="$(echo "$src" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + [[ -z "$src" ]] && continue + mkdir -p "$workdir/$(dirname "$src")" + printf '{"key":"value"}\n' > "$workdir/$src" + done < <(grep -E '^\s*-\s+file:' "$config") + + PTC_API_TOKEN='' "$CLI" --config-file "$config" --project-dir "$workdir" --dry-run 2>&1 +} + +assert_parses() { + local config="$1" name output + name="$(basename "$config")" + output="$(run_example "$config")" + + if echo "$output" | grep -q "Missing 'files:' section"; then + fail "$name is parsed as a config (got: Missing 'files:' section)" + return + fi + if echo "$output" | grep -q 'File not found'; then + fail "$name declares source paths the parser then cannot find" + echo "$output" | grep 'File not found' | sed 's/^/ /' + return + fi + if ! echo "$output" | grep -q 'Processing files from config'; then + fail "$name never reached file processing" + echo "$output" | tail -3 | sed 's/^/ /' + return + fi + pass "$name parses and reaches file processing" +} + +# `{lang}` is not a placeholder the CLI knows — substitute_pattern only expands +# `{{lang}}`. A single-brace example silently translates into a literal path. +assert_placeholder() { + local config="$1" name + name="$(basename "$config")" + if grep -E '(^|[^{])\{lang\}([^}]|$)' "$config" >/dev/null 2>&1; then + fail "$name uses {lang}; only {{lang}} is substituted" + else + pass "$name uses the {{lang}} placeholder" + fi +} + +# A token in a committed file is the thing the deprecation exists to prevent. +assert_no_token_key() { + local config="$1" name + name="$(basename "$config")" + if grep -qE '^\s*api_token:' "$config"; then + fail "$name teaches the deprecated api_token: key" + else + pass "$name keeps the token out of the file" + fi +} + +main() { + echo "Config examples — every file we tell people to copy" + echo "===================================================" + + local found=0 config + for config in "$EXAMPLES_DIR"/*.yml "$EXAMPLES_DIR"/*.yml.example; do + [[ -e "$config" ]] || continue + found=$((found + 1)) + assert_parses "$config" + assert_placeholder "$config" + assert_no_token_key "$config" + done + + if [[ $found -eq 0 ]]; then + fail "no examples found in $EXAMPLES_DIR" + fi + + echo + echo "Total: $test_count Passed: $passed_count Failed: $failed_count" + [[ $failed_count -eq 0 ]] || return 1 + return 0 +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi From 094cbeb152f76a3db883bd75d216d4f8d34d72ed Mon Sep 17 00:00:00 2001 From: Pavel Tkachenko Date: Thu, 6 Aug 2026 10:31:29 +0300 Subject: [PATCH 2/2] [ci18-7417] Run the tests that already existed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There was no CI here at all — no workflow file, nothing ran the suites on a push. They took four seconds and were fully offline; they simply never ran unless someone remembered. Meanwhile this repository ships the script customers download by tag, the recipes the product prints, and the copy ptc-action vendors and republishes as @v1, so a broken main becomes a release and a release becomes what every customer's pipeline fetches. The suites run twice. Once on ubuntu-latest with current bash, and once on macos-latest against /bin/bash, which is still 3.2.57 — the CLI targets 3.2 on purpose (busybox mktemp, ci18-7277) and current bash quietly accepts syntax 3.2 rejects, so a contributor on Linux cannot see that break. Each suite runs in its own log group and a failure in one does not hide the others. Three repository checks beyond the suites, each one a defect class ci18-7398 found by hand: - --version must agree with the VERSION constant. - No config/examples/*.config may come back; those were KEY=VALUE files that the parser rejected, and the extension is what suggested the wrong format. - Every fenced block in README.md and docs/*.md must be what it claims: yaml parses, bash survives bash -n. Output samples and code fragments belong in text. Verified against the pre-fix README: it catches both blocks that were tagged bash while showing CLI output. - Only {{lang}} may appear; a single-brace {lang} is taken literally and yields a path nobody has. Link checking is a separate advisory job. A dead link is a real defect, but an upstream hiccup must not turn main red. Fixing what the new checks found in docs/DEVELOPMENT.md: `git clone ` is not valid shell (< redirects) and the directory it cd'd into was never the repository name; two debugging examples used {lang}; the --timeout illustration is a case fragment, not a script, so it is text now. Also documented the CI and the offline property of the suites there. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/test.yml | 169 +++++++++++++++++++++++++++++++++++++ README.md | 2 + docs/DEVELOPMENT.md | 33 ++++++-- 3 files changed, 196 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ead1416 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,169 @@ +name: Self-test + +# ci18-7417. This repository ships a script customers download by tag, the +# recipes the product prints, and the copy ptc-action vendors and republishes +# as @v1 — so a broken main becomes a release, and a release becomes what every +# customer's pipeline fetches. The suites below already existed and took four +# seconds; nothing ran them. +on: + push: + branches: [main] + pull_request: {} + workflow_dispatch: {} + +jobs: + suites: + name: suites on ${{ matrix.label }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + # macOS still ships bash 3.2.57 and the CLI targets it on purpose, so + # it is not a nice-to-have: current bash accepts syntax 3.2 rejects, + # and a contributor on Linux cannot see the difference. + - os: macos-latest + label: bash 3.2 (macOS) + bash_bin: /bin/bash + - os: ubuntu-latest + label: current bash (Linux) + bash_bin: bash + steps: + - uses: actions/checkout@v7 + + - name: bash under test + env: + BASH_BIN: ${{ matrix.bash_bin }} + run: | + "$BASH_BIN" --version | head -1 + + - name: ptc-cli.sh parses + env: + BASH_BIN: ${{ matrix.bash_bin }} + run: | + "$BASH_BIN" -n ptc-cli.sh + + - name: every suite + env: + BASH_BIN: ${{ matrix.bash_bin }} + run: | + set -uo pipefail + shopt -s nullglob + suites=(tests/test-*.sh) + if [ ${#suites[@]} -eq 0 ]; then + echo "::error::no suites found — tests/test-*.sh matched nothing" + exit 1 + fi + failed=0 + for suite in "${suites[@]}"; do + echo "::group::$suite" + if "$BASH_BIN" "$suite"; then + echo "$suite OK" + else + failed=1 + echo "::error file=$suite::$suite failed" + fi + echo "::endgroup::" + done + exit "$failed" + + checks: + name: repository checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: --version agrees with VERSION + run: | + set -euo pipefail + declared=$(grep -m1 -oE 'readonly VERSION="[^"]+"' ptc-cli.sh | cut -d'"' -f2) + printed=$(./ptc-cli.sh --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') + echo "declared=$declared printed=$printed" + test "$declared" = "$printed" + + - name: config examples are all YAML the parser accepts + run: | + set -euo pipefail + # tests/test-config-examples.sh proves they parse; this only guards + # against the extension that started ci18-7398 coming back. + if compgen -G 'config/examples/*.config' > /dev/null; then + echo "::error::config/examples/*.config is back — these are YAML, not KEY=VALUE" + exit 1 + fi + echo "no .config files — OK" + + - name: every fenced block is what it claims to be + run: | + set -euo pipefail + python3 -m pip install --quiet pyyaml + python3 - <<'PY' + import glob, os, re, subprocess, sys, tempfile, yaml + + # ci18-7398: the README shipped a bash example that exited 1 on + # copy-paste and two CI blocks that could not run. A block tagged + # `yaml` must parse as YAML and one tagged `bash` must at least be + # syntactically valid — output samples and fragments belong in `text`. + bad = [] + total = 0 + for path in ['README.md'] + sorted(glob.glob('docs/*.md')): + blocks = re.findall(r'```(\w+)\n(.*?)```', open(path).read(), re.S) + total += len(blocks) + for i, (lang, body) in enumerate(blocks, 1): + first = body.splitlines()[0][:60] if body.strip() else '(empty)' + if lang == 'yaml': + try: + docs = list(yaml.safe_load_all(body)) + assert docs and docs[0], 'parsed to nothing' + except Exception as e: + bad.append(f'{path} block {i} (yaml, {first!r}): {e}') + elif lang == 'bash': + with tempfile.NamedTemporaryFile('w', suffix='.sh', delete=False) as f: + f.write(body) + tmp = f.name + r = subprocess.run(['bash', '-n', tmp], capture_output=True, text=True) + os.unlink(tmp) + if r.returncode: + bad.append(f'{path} block {i} (bash, {first!r}): {r.stderr.strip()}') + + if not total: + sys.exit('no fenced blocks found at all — check the parser') + if bad: + sys.exit('blocks that do not run as written:\n ' + '\n '.join(bad)) + print(f'{total} fenced blocks OK') + PY + + - name: only {{lang}} is used as the placeholder + run: | + set -euo pipefail + # substitute_pattern expands {{lang}} only; a single-brace {lang} is + # taken literally and produces a path nobody has (ci18-7398). + if grep -rnE '(^|[^{])\{lang\}([^}]|$)' README.md docs/ config/examples/ 2>/dev/null; then + echo "::error::single-brace {lang} found — only {{lang}} is substituted" + exit 1 + fi + echo "no single-brace placeholders — OK" + + links: + name: README and wiki links (advisory) + runs-on: ubuntu-latest + # Advisory: a dead link is a real defect, but an upstream hiccup must not + # turn main red. Read the log when this is yellow. + continue-on-error: true + steps: + - uses: actions/checkout@v7 + - name: every link resolves + run: | + set -uo pipefail + bad=0 + # Skip anything holding a shell variable — those are templates, not URLs. + for url in $(grep -ohE 'https?://[^)"`< ]+' README.md docs/*.md | sed 's/[.,]$//' | grep -v '\$' | sort -u); do + code=$(curl -s -o /dev/null -w '%{http_code}' -L --max-time 20 "$url" || echo 000) + printf '%-76s %s\n' "$url" "$code" + case "$code" in + 2*|3*) ;; + # The API base answers 404 by design; it is a base URL, not a page. + 404) case "$url" in */api/v1/) ;; *) bad=1 ;; esac ;; + *) bad=1 ;; + esac + done + exit "$bad" diff --git a/README.md b/README.md index 76a7035..f454c19 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # PTC CLI - Private Translation Cloud CLI +[![Self-test](https://github.com/OnTheGoSystems/ptc-cli/actions/workflows/test.yml/badge.svg)](https://github.com/OnTheGoSystems/ptc-cli/actions/workflows/test.yml) + Bash script for processing translation files through PTC (Private Translation Cloud) API with support for various project configurations. [Sample repositories](https://github.com/OnTheGoSystems/ptc-cli/wiki/Sample-repositories) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index e064cce..90f1f35 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -3,7 +3,7 @@ ## Project Structure ``` -ptc-cli-bash/ +ptc-cli/ ├── ptc-cli.sh # Main executable script ├── README.md # Main documentation ├── config/ # Configurations @@ -50,8 +50,8 @@ ptc-cli-bash/ ```bash # Clone project -git clone -cd ptc-cli-bash +git clone https://github.com/OnTheGoSystems/ptc-cli.git +cd ptc-cli # Set execution permissions chmod +x ptc-cli.sh @@ -72,11 +72,28 @@ for suite in tests/test-*.sh; do echo "== $suite"; bash "$suite" || break; done ./tests/test-exit-codes.sh ./tests/test-error-shapes.sh ./tests/test-rate-limit.sh +./tests/test-config-examples.sh -# Test specific functionality -./ptc-cli.sh -s en -p '{lang}-copy.json' --dry-run --verbose +# Test specific functionality ({{lang}} is the placeholder; a single-brace one is taken literally) +./ptc-cli.sh -s en -p '{{lang}}-copy.json' --dry-run --verbose ``` +The whole set is offline — four suites stub `curl`, the rest only use +`--dry-run`, which skips preflight — and finishes in about four seconds. + +### CI + +`.github/workflows/test.yml` runs every suite on push and on pull request +(ci18-7417). Two things worth knowing about it: + +- It runs the suites **twice**: on `ubuntu-latest` with current bash, and on + `macos-latest` with `/bin/bash`, which is still 3.2.57. The CLI targets 3.2 on + purpose, and current bash quietly accepts syntax 3.2 rejects — so a + contributor on Linux cannot see that break without this. +- It checks that every fenced block in `README.md` is what it claims to be: a + `yaml` block must parse, a `bash` block must survive `bash -n`. Blocks that + show CLI output belong in `text`. This is the ci18-7398 defect class. + `test-status-handling.sh` sources `ptc-cli.sh` and stubs `curl`, so it covers status parsing, polling and preflight without network access. It asserts on the number of requests made, not just the return code: the failure mode it guards @@ -88,13 +105,13 @@ like a timeout rather than an error. Enable verbose mode for debugging: ```bash -./ptc-cli.sh -s en -p '{lang}/**/*.json' --verbose --dry-run +./ptc-cli.sh -s en -p '{{lang}}/**/*.json' --verbose --dry-run ``` For additional bash debugging you can use: ```bash -bash -x ./ptc-cli.sh -s en -p '{lang}-copy.json' --dry-run +bash -x ./ptc-cli.sh -s en -p '{{lang}}-copy.json' --dry-run ``` ## Adding New Features @@ -109,7 +126,7 @@ bash -x ./ptc-cli.sh -s en -p '{lang}-copy.json' --dry-run Example of adding `--timeout` option: -```bash +```text # In variables section TIMEOUT=300