@chesterbrains/translify-cli 0.8.0 → 0.8.1

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 CHANGED
@@ -309,9 +309,97 @@ 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 published with placeholder errors (`--allow-errors`). `actor.type` is `user`, `apiKey`
347
+ (a CLI publish; `name` is the key's name) or `system`. A `ping` carries `id`, `event`, `createdAt`, `project` and
348
+ `webhook { id, target, destination }` only.
349
+
350
+ ### Verify a delivery
351
+
352
+ Generic webhooks `POST` the payload as `application/json` to an `https://` URL on a public host, with:
353
+
354
+ | Header | Value |
355
+ | --- | --- |
356
+ | `X-Translify-Event` | `publication.created` or `ping` |
357
+ | `X-Translify-Delivery` | the delivery id, identical on every attempt |
358
+ | `X-Translify-Timestamp` | unix seconds when the attempt was sent |
359
+ | `X-Translify-Signature` | `v1=<hex hmac-sha256(secret, "<timestamp>.<body>")>` |
360
+ | `User-Agent` | `translify-webhooks/1` |
361
+
362
+ The signing secret (`whsec_…`) is shown once when the webhook is created. Verify over the raw body, compare in constant
363
+ time and refuse a timestamp more than 300 s (5 minutes) away from now:
364
+
365
+ ```js
366
+ import { createHmac, timingSafeEqual } from 'node:crypto';
367
+
368
+ export function verify(rawBody, headers, secret) {
369
+ const timestamp = Number(headers['x-translify-timestamp']);
370
+ if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
371
+ const expected = `v1=${createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')}`;
372
+ const given = Buffer.from(String(headers['x-translify-signature'] ?? ''));
373
+ const wanted = Buffer.from(expected);
374
+ return given.length === wanted.length && timingSafeEqual(given, wanted);
375
+ }
376
+ ```
377
+
378
+ ### GitHub
379
+
380
+ A webhook with the **GitHub** target sends a
381
+ [`repository_dispatch`](https://docs.github.com/en/rest/repos/repos#create-a-repository-dispatch-event) instead:
382
+ `POST https://api.github.com/repos/<owner>/<repo>/dispatches` with `event_type` `translify.publication` (or
383
+ `translify.ping`) and the payload above as `client_payload`. No signature: GitHub authenticates the token.
384
+
385
+ The token is a fine-grained personal access token with **Contents: Read and write** on that one repository (that is
386
+ the permission `repository_dispatch` needs). Translify verifies it with a `translify.ping` dispatch before saving and
387
+ refuses a token or repository GitHub rejects; the expiry GitHub reports is shown in the list from 14 days out.
388
+
389
+ [`recipes/github-actions/translify-pull-on-publish.yml`](https://github.com/chesterbrains/translify-cli/blob/main/recipes/github-actions/translify-pull-on-publish.yml)
390
+ is the workflow that closes the loop: on `translify.publication` it runs `translify pull --from env:<slug>` for the
391
+ environment in the payload, commits, opens a pull request whose body carries the publish summary (batch, counts,
392
+ actor), runs your build and merges. Two things to set after copying it: the repository setting **Allow GitHub Actions
393
+ to create and approve pull requests** (Settings → Actions → General; without it the job token cannot open the PR),
394
+ and the environment the repository follows: a literal in the job's `if:`, and the webhook limited to that environment
395
+ in Translify, since every environment pulls into the same files. The workflow builds and merges inside the
396
+ same run because a pull request opened with `GITHUB_TOKEN` never triggers other workflows. The
397
+ [translify-examples](https://github.com/chesterbrains/translify-examples/blob/main/.github/workflows/translify-pull-on-publish.yml)
398
+ repository runs this workflow against the demo project.
399
+
312
400
  ## CI (GitHub Actions)
313
401
 
314
- Store the key in the repository secrets as `TRANSLIFY_SECRET_KEY`. Two ready-made workflows live in
402
+ Store the key in the repository secrets as `TRANSLIFY_SECRET_KEY`. Three ready-made workflows live in
315
403
  [`recipes/github-actions/`](https://github.com/chesterbrains/translify-cli/tree/main/recipes/github-actions); copy the
316
404
  one you need to `.github/workflows/` as it is.
317
405
 
@@ -323,8 +411,11 @@ one you need to `.github/workflows/` as it is.
323
411
  - [`translify-release.yml`](https://github.com/chesterbrains/translify-cli/blob/main/recipes/github-actions/translify-release.yml)
324
412
  is for apps that bundle translations at build time: `lint`, then `pull`, your build, then `publish` to the environment
325
413
  chosen in the Actions tab (or on a tag). Key scopes: `PULL` and `PUBLISH`.
414
+ - [`translify-pull-on-publish.yml`](https://github.com/chesterbrains/translify-cli/blob/main/recipes/github-actions/translify-pull-on-publish.yml)
415
+ runs when Translify's GitHub webhook announces a publish: `pull --from env:<slug>` for the published environment,
416
+ commit, a pull request with the publish summary, your build, merge. Key scope: `PULL`. Setup in [Webhooks](#webhooks).
326
417
 
327
- Both use `npx -y translify`, so they follow every 0.x release; pin a version (`npx -y @chesterbrains/translify-cli@0.6`) if a breaking
418
+ 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
419
  release must not turn the workflow red on its own.
329
420
 
330
421
  Fail a pull request when local files drift from Translify:
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Must equal package.json "version"; src/version.test.ts fails when they drift.
2
- export const VERSION = '0.8.0';
2
+ export const VERSION = '0.8.1';
3
3
  export const USER_AGENT = `translify-cli/${VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chesterbrains/translify-cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Push, pull and publish Translify translations from your repository",
5
5
  "license": "MIT",
6
6
  "repository": {