# SDK d'automatisation

Instrumentez les automatisations Node.js externes et les jobs Playwright natifs avec le SDK privé.

Le SDK TypeScript privé ajoute une télémétrie BernOps structurée aux automatisations Node.js externes et aux jobs Playwright gérés par BernOps. Ce guide couvre l'artéfact pris en charge `0.1.0-internal.3`. Le package est `UNLICENSED` et n'est pas publié dans un registre public. Les consommateurs externes installent le bundle de version conservé exact ; les jobs natifs déployés utilisent l'artéfact exact fourni par l'opérateur de leur worker BernOps.

## Prérequis et installation

- Utilisez Node.js 22 ou 24 pour les automatisations externes et la création locale de jobs natifs. Node.js 24 est recommandé.
- Pour une automatisation externe ou une vérification locale des types, demandez au responsable des versions BernOps les fichiers `.tgz`, `.tgz.sha256` et `.manifest.json` correspondants. Conservez les trois fichiers ensemble.
- Importez uniquement depuis `@bernops/automation`. Les consommateurs ESM, CommonJS et TypeScript sont pris en charge ; les imports internes ne le sont pas.

Vérifiez la somme de contrôle avant d'installer l'artéfact dans une automatisation externe ou un projet de création local :

```bash
export SDK_ARTIFACT_DIR=/chemin/absolu/vers/la-version-conservee
export SDK_TARBALL="$SDK_ARTIFACT_DIR/bernops-automation-0.1.0-internal.3.tgz"

cd "$SDK_ARTIFACT_DIR"
sha256sum --check "$(basename "$SDK_TARBALL").sha256"

cd /chemin/absolu/vers/votre-automatisation
npm install --offline --ignore-scripts --no-audit --no-fund \
  --registry=https://registry.invalid/ "$SDK_TARBALL"
npm ls @bernops/automation
```

Le fichier SHA-256 doit provenir de la même version approuvée que le manifeste. Un responsable disposant de la révision source BernOps correspondante peut exécuter la vérification complète du manifeste, du contenu du package, des déclarations et des consommateurs propres :

```bash
cd /chemin/absolu/vers/bernops/sdk/typescript
npm run artifact:verify -- --manifest \
  "$SDK_ARTIFACT_DIR/bernops-automation-0.1.0-internal.3.manifest.json"
```

**Utilisez un seul artéfact exact**

Pour les projets externes et locaux, ne remplacez pas le tarball par un lien workspace, une installation du répertoire source, un alias mutable, un import interne ou une solution de repli vers un registre public. Un job natif déployé n'utilise pas votre `node_modules` local ; son worker fournit le package. Si l'une de ces versions d'artéfact diffère de ce guide, utilisez la documentation et la politique de compatibilité correspondantes plutôt que de supposer sa compatibilité.


## Choisir un transport

| Emplacement de l'automatisation | Transport | Identifiant | Propriétaire du cycle de vie |
| --- | --- | --- | --- |
| CI, ordonnanceur, service ou script hors d'un worker BernOps | `external` | Clé API `ingest-runner` liée au job | SDK |
| Job Playwright distribué par un worker BernOps | `native` | Jeton de bouclage injecté par le worker ; aucune clé API | Worker |

Sélectionnez explicitement le transport avec `BernOps.fromEnv({ transport: 'external' })` ou `BernOps.fromEnv({ transport: 'native' })`. Utilisez `auto` uniquement si le déploiement garantit une seule configuration complète. La sélection échoue de manière fermée et ne tente jamais une solution de repli après une erreur d'authentification, TLS, d'identité ou de capacité.

## Démarrage rapide externe

Créez un job externe et une clé `ingest-runner`, puis stockez la clé en clair dans le gestionnaire de secrets du processus. Les scopes d'écriture minimaux sont `runs:write`, `runs:finish`, `logs:write`, `steps:write`, `metrics:write`, `progress:write` et `attachments:write`. Les scopes de lecture et d'administration ne sont pas requis par le SDK émetteur.

```bash
export BERNOPS_BASE_URL=https://bernops.example
export BERNOPS_API_KEY='secret-lie-au-job'
export BERNOPS_EXTERNAL_JOB_ID='uuid-job-externe'
export AUTOMATION_RUN_ID='tentative-ordonnanceur-123'
```

Définissez `AUTOMATION_RUN_ID` depuis un ID durable fourni par l'ordonnanceur, le système CI ou la tentative métier. Réutilisez cette valeur lorsque la même tentative est relancée ; ne générez pas un nouvel ID à chaque démarrage de processus.

```ts
import { BernOps, type DeliveryReport } from '@bernops/automation';

function requiredEnv(name: string): string {
  const value = process.env[name]?.trim();
  if (!value) throw new Error(`Variable d'environnement requise absente : ${name}`);
  return value;
}

function deliveryIsDegraded(report: DeliveryReport): boolean {
  return report.pending > 0 || report.failed > 0 ||
    report.dropped > 0 || report.timedOut;
}

const client = BernOps.fromEnv({
  transport: 'external',
  redaction: { secretEnvironmentVariables: ['CUSTOM_SERVICE_TOKEN'] },
  onDiagnostic(diagnostic) {
    // Le callback reçoit des diagnostics SDK bornés et déjà masqués.
    console.warn(`[bernops:${diagnostic.code}] ${diagnostic.message}`);
  },
});

try {
  await client.capabilities();
  await client.run(
    {
      externalRunId: requiredEnv('AUTOMATION_RUN_ID'),
      name: 'import nocturne',
      service: 'catalogue',
      environment: 'production',
    },
    async (run) => {
      const admission = run.log.info('import démarré');
      if (!admission.accepted) {
        console.warn(`Événement BernOps non mis en file : ${admission.reason}`);
      }

      await run.step('importer les enregistrements', async () => {
        run.metric.count('records.imported', 250, { unit: 'items' });
        run.progress({ current: 250, total: 500, stage: 'import' });
      });

      const report = await run.flush();
      if (deliveryIsDegraded(report)) console.warn('Livraison BernOps dégradée', report);
    }
  );
} finally {
  const report = await client.shutdown();
  if (deliveryIsDegraded(report)) console.warn('Arrêt BernOps dégradé', report);
}
```

`BERNOPS_EXTERNAL_JOB_ID` identifie le job BernOps enregistré. `externalRunId` identifie une tentative durable de ce job. Si `externalRunId` est omis, la valeur générée ne peut être rejouée de manière sûre que dans le processus courant.

Préférez `client.run()` : cette méthode ferme les étapes ouvertes, vide la télémétrie, termine en succès ou en échec une exécution externe et préserve la valeur retournée ou l'erreur exacte du callback. Utilisez `startRun()` uniquement lorsqu'un appelant externe doit gérer une exécution dans plusieurs fonctions ; attendez `complete()` ou `fail()` avant l'arrêt.

## Démarrage rapide Playwright natif

N'ajoutez pas de tarball ni de commande d'installation de package à un job Playwright BernOps enregistré. Une fois le bootstrap privé approuvé installé, lancez `bernops setup` et choisissez **3 (worker d'automatisation)** ou **4 (les deux)** ; le menu propose par défaut l'authentification seule. La [configuration unifiée du worker](/fr/docs/workers-and-pool#worker-setup) obtient une version immuable signée avec le SDK privé exact déjà intégré, vérifie la disponibilité réelle SDK/navigateur et exige une approbation distincte de l'espace pour `native_bridge_v1` ainsi qu'une connexion worker confirmée. Ce parcours ne demande ni build depuis un checkout, ni installation publique du SDK, ni montage d'artéfact. Une installation locale du package sert seulement à la création et à la vérification des types ; elle ne configure pas le worker déployé. Les anciens workers ne sont pas actualisés silencieusement. Demandez à l'opérateur de vérifier les versions disponibles et les capacités approuvées si l'import du package ou la capacité native est indisponible.

Le worker BernOps injecte la configuration du bridge pour chaque exécution. Ne définissez et ne copiez pas `AB_LOG_URL`, `BERNOPS_BRIDGE_TOKEN`, `BERNOPS_RUN_ID` ou `BERNOPS_PROJECT_ID`, et n'ajoutez pas de `BERNOPS_API_KEY` au job.

```ts
import { test, expect } from '@playwright/test';
import { BernOps } from '@bernops/automation';

test('checkout', async ({ page }) => {
  const client = BernOps.fromEnv({
    transport: 'native',
    onDiagnostic(diagnostic) {
      console.warn(`[bernops:${diagnostic.code}] ${diagnostic.message}`);
    },
  });

  try {
    const capabilities = await client.capabilities();
    expect(capabilities.lifecycleOwner).toBe('worker');

    await client.run({ name: 'checkout' }, async (run) => {
      run.log.info('ouverture du checkout');
      await run.step('soumettre la commande', async () => {
        await page.goto('/checkout');
      });
    });
  } finally {
    await client.shutdown();
  }
});
```

Le worker et Playwright gèrent le démarrage, le heartbeat et l'état terminal. `run.complete()` et `run.fail()` existent sur le type partagé, mais rejettent l'appel en mode natif avec `BernOpsCapabilityError` et `code === 'lifecycle_owned_by_worker'`. Appelez `capabilities()` avant tout comportement facultatif : les pièces jointes persistantes natives ne sont disponibles que si `persistentAttachments` vaut `true`, et les limites de taille retournées font autorité.

## Télémétrie, progression et pièces jointes

| API | Comportement |
| --- | --- |
| `run.log.debug/info/warn/error` | Capture un log structuré et retourne immédiatement son admission en file. |
| `run.step` / `run.startStep` | Crée des étapes gérées par callback ou terminées manuellement. Transmettez un vrai handle d'étape parente pour le travail détaché. |
| `run.metric.gauge/count/rate/duration` | Capture des métriques à faible cardinalité. Les durées sont exprimées en millisecondes. |
| `run.progress` | Signale une progression par compteur, pourcentage ou sans valeur déterminée. |
| `run.attach` | Calcule l'empreinte, téléverse et confirme de manière persistante une pièce jointe rejouable. Attendez toujours son résultat. |
| `run.flush` | Attend le travail accepté jusqu'au marqueur courant et laisse l'exécution modifiable. |

La progression peut être regroupée pendant son attente ; les trous de séquence sont donc valides. Elle ne détermine jamais le succès ou l'échec et BernOps ne fabrique pas une progression à 100 % pour une exécution active. Gardez les noms et tags de métriques à faible cardinalité : n'utilisez pas d'adresses e-mail, d'ID utilisateur, d'ID de requête, d'horodatages, d'URL complètes ou de valeurs libres comme noms ou tags.

Une pièce jointe doit utiliser exactement une source rejouable : `path`, des données `Uint8Array` ou `Blob`, ou une fabrique `open` qui retourne un nouveau flux non lu à chaque tentative de livraison.

```ts
// `run` est le handle fourni au callback de client.run().
const attachment = await run.attach({
  path: '/tmp/import-summary.json',
  fileName: 'import-summary.json',
  contentType: 'application/json',
});

console.log(attachment.attachmentId, attachment.sizeBytes, attachment.duplicate);
```

Les types de contenu pris en charge sont PNG, JPEG, WebP, texte brut, JSON, CSV et PDF. Ne modifiez pas les octets du chemin ou du flux avant la fin de `attach()`. La confirmation représente l'enregistrement persistant des métadonnées et du stockage, pas une simple admission en file.

## Livraison, erreurs et sécurité

Les méthodes de log, de métrique et de progression retournent un `EnqueueResult` ; `accepted: true` signifie uniquement que la file en mémoire a accepté l'événement. La livraison réseau s'observe avec `flush()`, les méthodes terminales externes et `shutdown()` grâce à un `DeliveryReport` contenant `delivered`, `duplicate`, `dropped`, `pending`, `failed`, `timedOut` et `droppedByReason`.

La politique par défaut `failurePolicy: 'diagnostic'` préserve le résultat de l'automatisation si la télémétrie se dégrade et signale le problème dans `onDiagnostic` et les rapports de livraison. Choisissez `failurePolicy: 'strict'` uniquement si un travail métier réussi doit échouer parce que la télémétrie requise n'a pas pu être livrée. En mode strict, les limites de livraison explicites peuvent lever `BernOpsDeliveryError` ; le SDK ne remplace cependant jamais une erreur initialement levée par votre callback.

Testez les classes d'erreur publiques, `code`, `retryable` et les champs sûrs propres à chaque classe plutôt que de comparer le texte du message :

| Erreur | Action habituelle |
| --- | --- |
| `BernOpsConfigurationError` | Corrigez une configuration de transport, d'URL, d'identifiant, de livraison ou de masquage incomplète ou ambiguë. |
| `BernOpsAuthenticationError` | Faites pivoter ou corrigez la clé, ou examinez l'affectation courante du worker. Ne relancez pas avec les mêmes identifiants rejetés. |
| `BernOpsValidationError` | Corrigez une télémétrie, progression, métrique ou pièce jointe invalide ou non prise en charge. |
| `BernOpsCapabilityError` | Adaptez le comportement aux capacités négociées et au propriétaire du cycle de vie. |
| `BernOpsStateError` | Arrêtez les appels d'étape ou terminaux contradictoires et vérifiez les états obsolètes. |
| `BernOpsTransportError` | Examinez `status`, `requestId`, `retryAfterMs` et `retryable`. |
| `BernOpsDeliveryError` | Examinez le `report` immuable retourné par une limite de livraison stricte. |
| `BernOpsFinalizedRunError` | Considérez la tentative durable comme déjà terminée ou utilisez volontairement un nouvel ID de tentative métier. |

- Les endpoints externes exigent HTTPS. HTTP n'est accepté que pour les URL de test littérales `127.0.0.1` ou `[::1]`, configurées explicitement avec `allowInsecureLoopback: true` ; les identifiants dans l'URL et les redirections sont rejetés.
- Le masquage est une défense supplémentaire. Ne capturez jamais d'en-têtes d'autorisation, de cookies, de jetons, de données personnelles, de données de carte ou de corps de requête et de réponse non limités.
- Les octets des pièces jointes ne sont pas masqués. Nettoyez les fichiers avant leur téléversement, évitez les captures d'écran inutiles et appliquez une rétention ainsi que des contrôles d'accès adaptés.
- Les files et tentatives sont bornées et en mémoire ; elles ne survivent pas à `SIGKILL`, à un manque de mémoire, à la perte de l'hôte ou à une coupure de courant.
- L'import du package n'installe aucun gestionnaire de signal de processus. L'hôte définit sa propre politique de signaux, mais chaque chemin normal doit attendre un `client.shutdown()` borné dans `finally`.

## Dépannage

| Symptôme | Vérification |
| --- | --- |
| Le transport est incomplet ou ambigu | Sélectionnez explicitement `external` ou `native` et retirez les variables appartenant à l'autre transport. |
| L'URL externe est rejetée | Utilisez HTTPS. Pour un test local, utilisez un constructeur explicite avec une adresse de bouclage littérale et `allowInsecureLoopback: true`. |
| L'authentification échoue | Vérifiez l'association job/projet et la rotation de la clé. Ne relancez pas avec des identifiants rejetés inchangés. |
| L'identité ou les capacités natives échouent | Créez un nouveau client dans l'affectation courante du worker ; ne réutilisez jamais des valeurs de bridge copiées. |
| Une exécution externe durable est finalisée | Réutilisez-la uniquement pour observer son résultat terminal antérieur, ou choisissez un nouvel ID pour une véritable nouvelle tentative métier. |
| La livraison reste en attente, est abandonnée, échoue ou expire | Examinez `onDiagnostic`, le rapport et `droppedByReason` ; corrigez le transport et les délais avant d'augmenter les limites de file. |
| La pièce jointe change ou ne peut pas être rouverte | Gardez les chemins et octets stables et retournez un nouveau flux non lu identique à chaque appel de la fabrique. |
| Le processus ne se termine pas | Attendez les pièces jointes, les limites de vidage ou terminales et `client.shutdown()` ; nettoyez ensuite les minuteurs et listeners gérés par l'hôte. |

Ne masquez pas les erreurs permanentes avec des tentatives illimitées, des scopes plus larges, la désactivation de TLS, le suivi des redirections, la journalisation brute des corps de réponse ou des files plus grandes.

## Références associées

- [Clés API et scopes](/fr/docs/api-keys-scopes)
- [Jobs Playwright](/fr/docs/playwright-jobs)
- [Variables runtime](/fr/docs/runtime-variables)
- [Workers et exécution](/fr/docs/workers-execution)
- [Référence API REST](/fr/docs/api)

L'artéfact installé contient ses propres `README.md`, `CHANGELOG.md`, `COMPATIBILITY.md`, `UPGRADING.md` et déclarations TypeScript. Utilisez ces fichiers versionnés et les types de la racine du package dans votre IDE pour obtenir les signatures exactes ; ne copiez pas une signature d'API provenant d'une autre révision du SDK.
