Releases: OnTheGoSystems/ptc-cli
Release list
v1.0.5 — the GitLab recipe runs on a CI runner, and the merge request carries only translations
The GitLab recipe we print could not translate anything on an actual GitLab runner. Everything here was found by running the recipe exactly as printed, on a detached HEAD, rather than by reading it.
The recipe never reached the API
A CI runner checks out a detached HEAD. There, git branch --show-current succeeds and prints an empty string — so the || fallbacks in get_current_branch were never reached, and validate_args rejected the empty file tag before a single request went out.
recipe as printed: EXIT=1 on disk: en.json <- nothing translated
same, with --file-tag-name: EXIT=0 on disk: de.json en.json fr.json
An empty answer is now treated as no answer: the runner's own branch variable is consulted (CI_COMMIT_REF_NAME, GITHUB_REF_NAME, BITBUCKET_BRANCH, BRANCH_NAME, CIRCLE_BRANCH), then main. That fixes the printed recipe, the standalone snippet, and every other CI whose checkout is detached — Bitbucket Pipelines and the Jenkins git plugin included.
The merge request carried the CLI itself
The recipe curled ptc-cli.sh into the project root and ran git add -A, so every merge request contained the script plus whatever an earlier job step had dirtied:
what it committed: .ptc-config.yml dist/bundle.js locales/de.json package-lock.json ptc-cli.sh
what it should: locales/de.json
And because that download was always a new file, git diff --cached --quiet never short-circuited — each run force-updated the merge request even when no translation had changed.
The download now goes to /tmp, and a new flag records what the run actually wrote:
--written-manifest FILE every written path, NUL-separated, repository-root-relative
which the recipe hands to git add --pathspec-from-file=FILE --pathspec-file-nul. git reads the file itself, so the recipe needs no arrays and no word splitting — its shell is busybox sh, since alpine has no bash until before_script installs it.
Two behaviours shaped that design, both verified in alpine:3.22 (git 2.49.1): an ignored path in the manifest makes git add exit 1 while still staging the rest, so the recipe needs || true or GitLab aborts the job; and a path holding a glob metacharacter stages the wrong file — messages[1].json also staged the caller's messages1.json — unless written as :(literal).
The unpack filter dropped documented file types
find "$dir" -type f -name "*.json" -o -name "*.po" -o ...
-o binds looser than the implicit -a, so -type f applied only to the first -name: a directory named *.po matched and was moved wholesale. The alternatives are now grouped, and the list covers what the docs promise — .php and .properties were both documented and both silently never reached disk. .xml, .strings and .resx are covered too.
[skip ci] was the wrong loop guard
GitLab honours it by creating no pipeline at all. That silenced the translation merge request's own pipeline — leaving the translations untested and, with "Pipelines must succeed" enabled, unmergeable — and a squash carried the marker into the default branch, silencing the whole project. The guard moved to rules:, which GitLab evaluates against the commit message without suppressing anything.
How this was checked
tests/test-ci-recipe.sh runs the printed recipe end to end against a mock PTC API, with only curl and git push shimmed, and asserts what the merge request would contain: translation on a detached HEAD; only translations committed, with the fixture dirtying dist/bundle.js and package-lock.json first; the CLI neither committed nor left behind; no push when a second identical run writes nothing; and the shape of the push itself.
The detached-HEAD behaviour is now pinned by a test that previously documented it as a known defect.
Verifying the download
sha256 d5b1b2c62c530f43d76aa924e399548379369e458955a31bd11539580d71428d ptc-cli.sh
curl -fsSL https://raw.githubusercontent.com/OnTheGoSystems/ptc-cli/v1.0.5/ptc-cli.sh -o ptc-cli.sh
shasum -a 256 ptc-cli.sh # sha256sum on Linux9 suites, 0 failures, on bash 3.2 (macOS) and on current bash (Linux).
v1 points here. v1.0.4 and earlier are unchanged if you pin them.
v1.0.4 — the first run works, and a rate limit no longer costs you a file
Two defects that both landed on the run where a user is still deciding whether this works: the first CI run, and the sixth file.
The GitLab recipe's first run opened no merge request
The recipe ptc init prints ran git diff --quiet before git add -A. On a first run the translations are new files, and a plain git diff does not look at those — so the check reported nothing to do, the push was skipped, and the job went green having produced nothing. The second run worked, because by then the files were tracked.
So the one run that fails is the one a user judges the product by, and the log says nothing about it.
The recipe now stages first and checks git diff --cached --quiet, which is what the snippet published in the product already did. The self-test asserts the order of the two commands rather than the presence of both, and fails on the previous revision.
A rate limit no longer costs you a file
create, process and bulk share one bucket of 10 requests per minute, and a run spends two of them per file. Every project past five files therefore meets a 429 partway through — and the CLI treated it as a failed upload and carried on. The run ended with some files registered and some not, with HTTP 429 as the only clue in the log.
A 429 now asks for a retry instead of reporting failure. The wrapper waits and calls again, up to 5 times, honouring Retry-After when the server sends one and otherwise backing off 15s, 30s, 45s, 60s towards the one-minute window the limit is measured over. PTC does not send Retry-After today, so the backoff is what runs; the header path is there because the answer to "who paces this" belongs to the server the moment it wants it back.
Verified end to end against a mock that 429s the first two attempts of every call: the run waits, retries, and downloads the translations, where before it lost the file.
Failures say what went wrong
source_files create and process answer with {"error": "..."}. The CLI read only message, which turned those into a bare HTTP 422 in the CI log — the one place the reason was actually needed. Both shapes are now read.
The documentation is runnable
Three shipped config examples could not be used at all, and the docs described behaviour the CLI does not have. detect_config is reachable on the default API host without a token, but the README, show_init_help and the 404 hint all told the reader to point --api-url somewhere else first — a note standing between a new user and their first successful run. The quick start also passed a token to init, which needs none.
Verifying the download
sha256 29f66e8a3b89521e8e1e6a1e91e3a6ea014c6258da8518ac0694cb6cf5694945 ptc-cli.sh
curl -fsSL https://raw.githubusercontent.com/OnTheGoSystems/ptc-cli/v1.0.4/ptc-cli.sh -o ptc-cli.sh
shasum -a 256 ptc-cli.sh # sha256sum on LinuxThe suites now run on every push, not only on demand. 8 suites, 0 failures, on bash 3.2 (macOS) and on current bash (Linux).
v1.0.3 and earlier are unchanged if you pin them. v1 has since moved on to v1.0.5.
v1.0.3 — the GitLab recipe actually runs
Both fixes in this release were found by running our own GitLab snippet on a real GitLab runner — neither is reachable from a GitHub runner, which is why they survived v1.0.2.
ptc init prints an inline GitLab job, not a component address
include: component: is resolved by the customer's own GitLab — the $CI_SERVER_FQDN in a component address is always their server. A component we publish on one instance is therefore unreachable from gitlab.com and from every self-hosted instance, so the address ptc init used to print could never resolve for anyone. It was not published anywhere either.
ptc init now prints the job inline: same alpine:3.22 image, same loop-safe rules, same stable ptc/translations branch and MR-via-push-options. The CLI is pinned to this script's own version tag, so the recipe follows each release instead of drifting.
The GitLab push can actually succeed now
Two defects on the last step — after the translation has run and spent credits.
CI_JOB_TOKEN cannot push to its own repository unless a maintainer enables Settings → CI/CD → Job token permissions → "Allow Git push requests to the repository" (GitLab 18.4+, off by default; before 18.4 it was not possible at all). Project access tokens are the usual answer but are unavailable on gitlab.com Free. The snippet now reads PTC_GIT_PUSH_TOKEN — a write_repository token — and falls back to CI_JOB_TOKEN, so one recipe works on whichever route the customer's instance and plan allow. The opt-in is documented inline in the job.
The loop guard was [skip translations], which GitLab does not recognise — only [skip ci] and [ci skip] are honoured. The commit message now carries [skip ci]; the branch rule, which is what actually prevents the translation push from re-triggering the job, is unchanged.
The download works on busybox, not just GNU
mktemp on busybox rejects a template with anything after the XXXXXX — and busybox is alpine, which is the image our own GitLab snippet recommends. The job translated the files and then died on the download with mktemp: : Invalid argument and an empty HTTP code. Invisible on GitHub runners: they are Ubuntu, and GNU mktemp accepts the suffix.
unzip identifies the archive by content, so dropping .zip from the template costs nothing.
Verifying the download
sha256 87efed00bd9345b9a4d5fb1972d8d53525246f6a2a4e6a48ae7d20d67e41362a ptc-cli.sh
curl -fsSL https://raw.githubusercontent.com/OnTheGoSystems/ptc-cli/v1.0.3/ptc-cli.sh -o ptc-cli.sh
shasum -a 256 ptc-cli.sh # sha256sum on Linuxv1 now points here. v1.0.2, v1.0.1 and v1.0.0 are unchanged if you pin them.
180 tests, 0 failures (test-runner 12, test-init 72, test-exit-codes 22, test-error-shapes 22, test-status-handling 52).
v1.0.2 — ptc init: no token, action-based snippets
ptc init improvements, part of the CI on-ramp epic (ci18-7249). The translate pipeline is unchanged from v1.0.1.
ptc init no longer requires a token (ci18-7276)
detect_config is anonymous, and generating a config is the step you run before you have a token — so demanding one blocked the exact first-use path. With the API-token paywall in place, a trial org that cannot mint a token can still run ptc init and get a working .ptc-config.yml. When no token is set, no Authorization header is sent at all.
ptc init prints the ptc-action recipe (ci18-7254)
The old snippets curled the CLI from a floating v1.0.0 tag with no checksum, used actions/checkout@v4, and the GitLab job had no image and no bash — it would not start on a default Alpine runner. All three are gone. Both snippets now run through ptc-action:
- uses: actions/checkout@v7
- uses: OnTheGoSystems/ptc-action@v1
with: { api-token: ..., config-file: .ptc-config.yml, create-pr: true }The action SHA-pins the CLI and its dependencies, ships the right image, and is loop-safe. It is the same recipe the in-product token screen prints, so the CLI, the product and the action README no longer drift.
The CLI stays self-contained — ptc init ends by spelling out ./ptc-cli.sh --config-file .ptc-config.yml for anyone who does not want the action.
v1 now points here; v1.0.1 and v1.0.0 are unchanged if you pin them. 171 examples, 0 failures.
v1.0.1 — the CLI's verdict is now trustworthy
Everything here is about one thing: making the CLI's report of what happened match what actually happened. Details in ci18-7342.
The failure this removes
A rejected PUT /source_files/process used to print "File processing started successfully", then poll a file that was never queued until timeout, and exit 0 — a green pipeline that translated nothing. A rejected download saved the JSON error envelope as a .zip and tried to unpack it.
Changes
Reads both shapes of a rejected response. The API answers either HTTP 200 carrying {"success":false,...,"code":422} (older builds) or a real 422 (newer). The CLI only looked at the status, so the first shape read as success. Production and staging do not switch over on the same day, so it now reads both identically.
The exit code is the verdict. A partial run is a failure — one completed file out of ten used to exit 0. Failed or unfinished files now exit 1 with a summary naming the counts. --action status keeps exit 0 for "still translating", which is a legitimate answer, but not for a terminal failure.
A file is done when its translations are on disk. It used to be marked completed before downloading. HTTP 202 (archive not ready yet) now keeps the file in the polling loop rather than passing for success or failure.
monitor_interval / monitor_max_attempts work from .ptc-config.yml. Documented from the start, but only ever implemented as flags — and neither the GitHub action nor the GitLab component passes flags, so the ~8-minute polling ceiling was unreachable from CI.
Removed the --files/--outputs mode. Its flags were never wired into the parser, so 302 lines had never executed. Four of the five tests covering it passed only because the CLI rejected the unknown flag before reaching the assertion. --config-file already covers the same ground.
README documents exit codes.
Verified end to end against a live server
| scenario | result |
|---|---|
| full cycle | real de.json + ru.json written, exit 0 |
| malformed source file | exit 1, HTTP 422 ... error codes: [9001] |
| archive not ready (202) | keeps polling, then downloads cleanly |
| unfinished run | exit 1, Run incomplete: 0 completed, 1 failed |
162 examples, 0 failures across five suites.
Upgrading: v1 now points here. If you pin v1.0.0 it is unchanged. Expect previously-green pipelines that were silently producing nothing to start failing — that is the point.
v1.0.0
First tagged release of the PTC CLI (ptc-cli.sh). VERSION is 1.0.0.
What's in 1.0.0
- Reliability (ci18-7251 §1+§2): stop polling terminal translation states (fixes the ~8-minute phantom timeout); preflight check that validates the token and reports balance before uploading.
ptc init(ci18-7268): scaffolds.ptc-config.ymlfromPOST /api/v1/detect_config— scans the repo (respecting.gitignore/.ptcignore, paths only), writes the config plus a ready-to-paste CI snippet.- Supply chain (§5): every API request sends
User-Agent: ptc-cli/1.0.0; CI snippets and docs pin the download to this release tag. - Token hygiene (§6): token is env-first via
PTC_API_TOKEN(or--api-token); theapi_token:config key is deprecated (warn-and-ignore).
Pinned download
curl -fsSL https://raw.githubusercontent.com/OnTheGoSystems/ptc-cli/v1.0.0/ptc-cli.sh -o ptc-cli.sh
chmod +x ptc-cli.shUse v1.0.0 for an exact pin, or the floating v1 tag for backward-compatible updates.
Verify integrity
SHA256 (ptc-cli.sh) = ea808d3870a8539bbe5a509f1356f9288e58c46eadb08a8ed037d0db2afec586
shasum -a 256 ptc-cli.sh # macOS
sha256sum ptc-cli.sh # LinuxOr with the attached checksum file: shasum -a 256 -c ptc-cli.sh.sha256.
PTC CLI demo release
Demo release (not ready for production).