@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 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`. Two ready-made workflows live in
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
- 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
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
@@ -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.2';
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.2",
4
4
  "description": "Push, pull and publish Translify translations from your repository",
5
5
  "license": "MIT",
6
6
  "repository": {