Skip to content

The GitLab recipe now runs on a CI runner, and the merge request carries only translations - #18

Merged
pavel-te merged 3 commits into
mainfrom
fix/gitlab-recipe-runs-and-commits-only-translations
Aug 27, 2026
Merged

The GitLab recipe now runs on a CI runner, and the merge request carries only translations#18
pavel-te merged 3 commits into
mainfrom
fix/gitlab-recipe-runs-and-commits-only-translations

Conversation

@pavel-te

Copy link
Copy Markdown
Collaborator

The recipe could not translate anything

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 the empty file tag was rejected by validate_args — before a single request to the API.

Reproduced against a mock API, on a detached HEAD, with the recipe's exact command:

recipe as printed (no --file-tag-name):   EXIT=1   on disk: en.json
same, with --file-tag-name:               EXIT=0   on disk: de.json en.json fr.json

So every push to the default branch gave a red job, uploaded nothing and opened no merge request. Four of the five copies of this recipe carried the defect; only the one the product prints passed --file-tag-name and worked.

An empty answer is now treated as no answer: the runner's own branch variable is consulted (GitLab, GitHub, Bitbucket, Jenkins, CircleCI), then main. That fixes the printed recipe, the standalone snippet, and every other CI whose checkout is detached.

The merge request carried more than translations

The recipe curled the CLI into the project root and then ran git add -A:

=== what landed in the merge request ===
.ptc-config.yml
locales/de.json
ptc-cli.sh          <- the CLI the recipe had just downloaded

And because that download is always a new file, git diff --cached --quiet never short-circuited — every run force-updated the merge request even when no translation had changed.

The CLI now downloads outside the checkout, and --written-manifest FILE records what the run wrote: NUL-separated, repository-root-relative, for git add --pathspec-from-file=FILE --pathspec-file-nul. git reads it directly, so the recipe needs no arrays or word splitting — its shell is busybox sh, since alpine has no bash until before_script installs it.

Verified in alpine:3.22 (git 2.49.1): a path with a glob metacharacter stages the wrong file unless written as :(literal), and an ignored path makes git exit 1 while still staging the rest — hence || true, without which GitLab aborts the job.

The unpack filter dropped documented types

in the archive:  a.json  b.php  c.properties  dir.po/(a directory)
the filter gave: a.json  dir.po

.php is documented as an additional_translation_files companion, .properties as a source pattern — neither reached disk. And -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.

[skip ci] left the commit message

GitLab honours it by creating no pipeline at all, which silenced the translation merge request's own pipeline — the translations could not be tested before merge, and with "Pipelines must succeed" enabled the MR could never be merged. A squash carried it into the default branch and silenced the whole project. The guard now lives in rules:, which GitLab evaluates without suppressing anything.

Tests

tests/test-ci-recipe.sh executes the printed recipe against a mock PTC API on localhost — no docker, ~2 s — with only curl and git push shimmed, and asserts what the merge request would contain. Eight cases: translation on a detached HEAD, only-translations-committed, the CLI neither committed nor left behind, no push when nothing changed, and the push shape.

tests/test-git-context.sh now pins the detached-HEAD behaviour it previously documented as a known defect and deferred. tests/test-init.sh gained a check that the README's copy of the recipe is the one the CLI prints — these copies have drifted three ways before.

All nine suites green.

The GitLab recipe this CLI printed could not translate anything.

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 the empty file tag was rejected by
validate_args before a single request to the API. Every recipe that did not
pass --file-tag-name by hand produced a red job, uploaded nothing and opened no
merge request. Reproduced against a mock API, on a detached HEAD, with the
recipe's exact command: exit 1 and nothing on disk, versus exit 0 and the
translations written once a tag is supplied.

An empty answer is now treated as no answer: the runner's own branch variable
is consulted (GitLab, GitHub, Bitbucket, Jenkins, CircleCI), and only then does
it fall back to "main". That fixes the printed recipe, the standalone snippet,
and every other CI whose checkout is detached.

The merge request also carried more than translations. The recipe curled the
CLI into the project root and then ran `git add -A`, so every merge request
contained ptc-cli.sh itself, plus whatever an earlier step in the caller's job
had left in the working directory. And because that download was always a new
file, the `git diff --cached --quiet` guard never short-circuited: each run
force-updated the merge request even when no translation had changed.

The CLI is now downloaded outside the checkout, and a new --written-manifest
FILE records what the run wrote - NUL-separated, repository-root-relative, for
`git add --pathspec-from-file=FILE --pathspec-file-nul`. git reads it directly,
so the recipe needs no arrays or word splitting; the recipe's own shell is
busybox sh, since alpine has no bash until before_script installs it. Paths
holding a glob metacharacter are written as :(literal), because `git add --`
stops option parsing but not globbing, and a path outside the repository root
is not recorded at all - a pathspec matching nothing is fatal and would discard
every other translation staged in the same call.

The unpack filter dropped documented file types. `.php` is documented as an
additional_translation_files companion and `.properties` as a source pattern;
neither was in the find filter, so they never reached disk. And `-o` binds
looser than the implicit `-a`, so `-type f` applied only to the first `-name`:
a DIRECTORY named e.g. "x.po" matched and was moved wholesale.

[skip ci] left the commit message. GitLab honours it by creating no pipeline at
all, which silenced the translation merge request's own pipeline - leaving the
translations untested and, with "Pipelines must succeed" enabled, unmergeable -
and a squash carried it into the default branch, silencing the whole project.
The loop guard now lives in rules:, which GitLab evaluates without suppressing
anything.

Tests: tests/test-ci-recipe.sh runs the printed recipe end to end against a
mock API on localhost - no docker - with only curl and git push shimmed, and
asserts what the merge request would contain. tests/test-git-context.sh now
pins the detached-HEAD behaviour it previously documented as a known defect,
and tests/test-init.sh checks that the README's copy of the recipe is the one
the CLI prints.
On a hosted macOS runner the mock did not come up inside the old ~4s window, so
the suite printed "skipped" and the job stayed green - the recipe went
uncovered on that platform and nothing said so. The wait is now ~12s, a failure
prints the mock's own output, and killing the background process no longer
leaves a "Terminated" line that reads like a failure.
Binding 0.0.0.0 is refused by the firewall on a hosted macOS runner: the mock
process started, never listened, and the suite skipped itself while the job
stayed green. Everything that talks to this mock is on the same host - under
act the job container shares the VM's network namespace - so loopback is
enough. PTC_MOCK_BIND overrides it.
@pavel-te
pavel-te merged commit 3521751 into main Aug 27, 2026
8 checks passed
@pavel-te
pavel-te deleted the fix/gitlab-recipe-runs-and-commits-only-translations branch August 27, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants