Skip to content

Releases: OnTheGoSystems/ptc-cli

v1.0.5 — the GitLab recipe runs on a CI runner, and the merge request carries only translations

Choose a tag to compare

@pavel-te pavel-te released this 31 Aug 09:27
3521751

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 Linux

9 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

Choose a tag to compare

@pavel-te pavel-te released this 31 Aug 09:27

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 Linux

The 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

Choose a tag to compare

@pavel-te pavel-te released this 30 Jul 09:42

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 Linux

v1 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

Choose a tag to compare

@pavel-te pavel-te released this 24 Jul 05:57

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-containedptc 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

Choose a tag to compare

@pavel-te pavel-te released this 23 Jul 14:41

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

Choose a tag to compare

@pavel-te pavel-te released this 16 Jul 11:48
7ae60fa

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.yml from POST /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); the api_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.sh

Use 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        # Linux

Or with the attached checksum file: shasum -a 256 -c ptc-cli.sh.sha256.

PTC CLI demo release

PTC CLI demo release Pre-release
Pre-release

Choose a tag to compare

@pavel-te pavel-te released this 15 Aug 10:36

Demo release (not ready for production).