@chesterbrains/translify-cli 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -309,9 +309,101 @@ When the project has **Require review** turned on in Translify:
|
|
|
309
309
|
- `publish` refuses when a translation that would go live has a placeholder error (a missing or extra placeholder, invalid ICU, a printf type mismatch). Nothing is published, the refused translations are listed like `lint` lists them, and the command exits 6. Warnings never block. On an environment with **Publish approved text only**, only approved translations count: one carried over from its last approved text never blocks. `--allow-errors` publishes anyway and reports how many went out with errors; the publish is recorded in the activity log with the override.
|
|
310
310
|
- `pull --only-approved` writes the approved text from the working copy and leaves out translations that were never approved, for building a release bundle without publishing. `status --only-approved` compares against the same. It works with `--from working` only: environments already apply the gate.
|
|
311
311
|
|
|
312
|
+
## Webhooks
|
|
313
|
+
|
|
314
|
+
A project's admins can add webhooks under **Integrations**. Translify calls them with `publication.created` every time
|
|
315
|
+
an environment's live batch changes: a publish, a rollback or an unpublish (`batch.kind` says which). A publish whose
|
|
316
|
+
result is identical to what is live emits nothing. `ping` is the test event, sent by "Send test" and by the
|
|
317
|
+
verification dispatch that saves a GitHub webhook.
|
|
318
|
+
|
|
319
|
+
Deliveries are made by a worker, not by the request that published: five attempts, waiting 1 minute, 5 minutes,
|
|
320
|
+
30 minutes and 2 hours between them, then the delivery is `DEAD` and can be resent from the app. The log keeps 30
|
|
321
|
+
days. The delivery id is the same on every attempt (`X-Translify-Delivery` and the payload's `id`), so a receiver that
|
|
322
|
+
answers slowly and gets the retry can dedupe on it. Answer 2xx as soon as the request is accepted and do the work
|
|
323
|
+
afterwards; the sender waits 10 seconds.
|
|
324
|
+
|
|
325
|
+
### Payload
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"id": "<delivery id>",
|
|
330
|
+
"event": "publication.created",
|
|
331
|
+
"createdAt": "2026-10-25T14:02:11.000Z",
|
|
332
|
+
"project": { "id": "…", "slug": "shop", "name": "Shop" },
|
|
333
|
+
"environment": { "id": "…", "slug": "production", "name": "Production", "requireApproved": true },
|
|
334
|
+
"batch": {
|
|
335
|
+
"id": "…", "seq": 41, "kind": "PUBLISH",
|
|
336
|
+
"publishedCount": 1280, "addedCount": 3, "changedCount": 14, "removedCount": 0,
|
|
337
|
+
"withheldCount": 0, "errorCount": 0
|
|
338
|
+
},
|
|
339
|
+
"actor": { "type": "user", "id": "…", "name": "Anna Rossi" }
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`addedCount`, `changedCount` and `removedCount` are the batch's diff against the previous live batch: a first publish
|
|
344
|
+
has everything in `addedCount`; an `UNPUBLISH` has `publishedCount: 0` and everything that went offline in
|
|
345
|
+
`removedCount`. "Did anything change" is the sum of the three. `withheldCount` is what a `requireApproved` gate kept
|
|
346
|
+
back, `errorCount` the cells that went live with placeholder errors (a publish with `--allow-errors` or the app's
|
|
347
|
+
"publish anyway", or a rollback, which skips the gate). `actor.type` is `user`, `apiKey`
|
|
348
|
+
(a CLI publish; `name` is the key's name) or `system`. A `ping` carries `id`, `event`, `createdAt`, `project` and
|
|
349
|
+
`webhook { id, target, destination }` only.
|
|
350
|
+
|
|
351
|
+
### Verify a delivery
|
|
352
|
+
|
|
353
|
+
Generic webhooks `POST` the payload as `application/json` to an `https://` URL on a public host, with:
|
|
354
|
+
|
|
355
|
+
| Header | Value |
|
|
356
|
+
| --- | --- |
|
|
357
|
+
| `X-Translify-Event` | `publication.created` or `ping` |
|
|
358
|
+
| `X-Translify-Delivery` | the delivery id, identical on every attempt |
|
|
359
|
+
| `X-Translify-Timestamp` | unix seconds when the attempt was sent |
|
|
360
|
+
| `X-Translify-Signature` | `v1=<hex hmac-sha256(secret, "<timestamp>.<body>")>` |
|
|
361
|
+
| `User-Agent` | `translify-webhooks/1` |
|
|
362
|
+
|
|
363
|
+
The signing secret is shown once when the webhook is created: a generated `whsec_…`, or the 16 to 255 characters you
|
|
364
|
+
supplied. Verify over the raw body, compare in constant time and refuse a timestamp more than 300 s (5 minutes) away
|
|
365
|
+
from now:
|
|
366
|
+
|
|
367
|
+
```js
|
|
368
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
369
|
+
|
|
370
|
+
export function verify(rawBody, headers, secret) {
|
|
371
|
+
const timestamp = Number(headers['x-translify-timestamp']);
|
|
372
|
+
if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
|
|
373
|
+
const expected = `v1=${createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')}`;
|
|
374
|
+
const given = Buffer.from(String(headers['x-translify-signature'] ?? ''));
|
|
375
|
+
const wanted = Buffer.from(expected);
|
|
376
|
+
return given.length === wanted.length && timingSafeEqual(given, wanted);
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### GitHub
|
|
381
|
+
|
|
382
|
+
A webhook with the **GitHub** target sends a
|
|
383
|
+
[`repository_dispatch`](https://docs.github.com/en/rest/repos/repos#create-a-repository-dispatch-event) instead:
|
|
384
|
+
`POST https://api.github.com/repos/<owner>/<repo>/dispatches` with `event_type` `translify.publication` (or
|
|
385
|
+
`translify.ping`) and the payload above as `client_payload`. No signature: GitHub authenticates the token.
|
|
386
|
+
|
|
387
|
+
The token is a fine-grained personal access token with **Contents: Read and write** on that one repository (that is
|
|
388
|
+
the permission `repository_dispatch` needs). Translify verifies it with a `translify.ping` dispatch before saving and
|
|
389
|
+
refuses a token or repository GitHub rejects; the expiry GitHub reports is shown in the list from 14 days out.
|
|
390
|
+
|
|
391
|
+
[`recipes/github-actions/translify-pull-on-publish.yml`](https://github.com/chesterbrains/translify-cli/blob/main/recipes/github-actions/translify-pull-on-publish.yml)
|
|
392
|
+
is the workflow that closes the loop: on `translify.publication` it runs `translify pull --from env:<slug>` for the
|
|
393
|
+
environment in the payload, commits, opens a pull request whose body carries the publish summary (batch, counts,
|
|
394
|
+
actor), runs the build step you add and merges. Three things to set after copying it: the repository setting **Allow
|
|
395
|
+
GitHub Actions to create and approve pull requests** (Settings → Actions → General; without it the job token cannot
|
|
396
|
+
open the PR); the environment the repository follows, a literal in the job's `if:`, with the webhook limited to that
|
|
397
|
+
environment in Translify since every environment pulls into the same files; and your build in the commented Build
|
|
398
|
+
step, so a pull that does not compile leaves the pull request open instead of merging. The workflow builds and merges
|
|
399
|
+
inside the same run because a pull request opened with `GITHUB_TOKEN` never triggers other workflows; a protected
|
|
400
|
+
default branch needs the Merge step removed or a bypass for the token. The
|
|
401
|
+
[translify-examples](https://github.com/chesterbrains/translify-examples/blob/main/.github/workflows/translify-pull-on-publish.yml)
|
|
402
|
+
repository runs this workflow against the demo project.
|
|
403
|
+
|
|
312
404
|
## CI (GitHub Actions)
|
|
313
405
|
|
|
314
|
-
Store the key in the repository secrets as `TRANSLIFY_SECRET_KEY`.
|
|
406
|
+
Store the key in the repository secrets as `TRANSLIFY_SECRET_KEY`. Three ready-made workflows live in
|
|
315
407
|
[`recipes/github-actions/`](https://github.com/chesterbrains/translify-cli/tree/main/recipes/github-actions); copy the
|
|
316
408
|
one you need to `.github/workflows/` as it is.
|
|
317
409
|
|
|
@@ -323,8 +415,11 @@ one you need to `.github/workflows/` as it is.
|
|
|
323
415
|
- [`translify-release.yml`](https://github.com/chesterbrains/translify-cli/blob/main/recipes/github-actions/translify-release.yml)
|
|
324
416
|
is for apps that bundle translations at build time: `lint`, then `pull`, your build, then `publish` to the environment
|
|
325
417
|
chosen in the Actions tab (or on a tag). Key scopes: `PULL` and `PUBLISH`.
|
|
418
|
+
- [`translify-pull-on-publish.yml`](https://github.com/chesterbrains/translify-cli/blob/main/recipes/github-actions/translify-pull-on-publish.yml)
|
|
419
|
+
runs when Translify's GitHub webhook announces a publish: `pull --from env:<slug>` for the published environment,
|
|
420
|
+
commit, a pull request with the publish summary, your build, merge. Key scope: `PULL`. Setup in [Webhooks](#webhooks).
|
|
326
421
|
|
|
327
|
-
|
|
422
|
+
All three use `npx -y translify`, so they follow every 0.x release; pin a version (`npx -y @chesterbrains/translify-cli@0.6`) if a breaking
|
|
328
423
|
release must not turn the workflow red on its own.
|
|
329
424
|
|
|
330
425
|
Fail a pull request when local files drift from Translify:
|
package/dist/version.js
CHANGED