Aller au contenu
English

Lancer du travail depuis des webhooks

Vérifiez les événements reçus et publiez les jobs du workflow correspondant dans une file.

Utilisez une source de webhook pour vérifier la requête reçue, puis associez l’événement accepté à un job de la file. serveTriggers() traite la requête HTTP ; les workers exécutent le workflow après la publication.

import type { TriggerRoute } from "@elie-laloum/outpost";
import { labelAdded } from "@elie-laloum/outpost";

export const on: TriggerRoute["on"] = (event) => {
  const issue = labelAdded(event, "outpost:fix");
  if (!issue) return undefined;
  return {
    handler: "fix",
    runId: `${issue.repository}#${issue.number}`,
    input: { repository: issue.repository, issue: issue.number },
  };
};
import { createGithubWebhook } from "@elie-laloum/outpost";
import { on } from "./label-job.ts";

export const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!secret) throw new Error("Set GITHUB_WEBHOOK_SECRET");
export const routes = [
  { path: "/github", source: createGithubWebhook({ secret }), on },
];
import { reportValue } from "./reporter.ts";
import { createSqliteTaskQueue, serveTriggers } from "@elie-laloum/outpost";
import { routes } from "./github-route.ts";

export const queue = await createSqliteTaskQueue(".outpost/jobs.sqlite");
export const server = await serveTriggers({
  queue,
  port: 8787,
  routes,
  onError: (error, failure) => console.error(failure, error),
});
reportValue(`Listening on ${server.url}`);
// Example output: Listening on http://127.0.0.1:8787

Ajouter le label outpost:fix à une issue ou à une pull request publie un job fix dans la file. Un worker l’exécute avec defineWorkflowJob() : voir Files de jobs et workers.

Glissez pour vous déplacer · Ctrl + molette pour zoomer
100 %
  • ServeurRépond à l’émetteur en quelques secondes.
    1. VérifierLa source contrôle la signature, sinon la réponse est 401. createGithubWebhook()
    2. Routeron(event) renvoie un job, ou undefined pour ignorer l’événement. labelAdded()commandIssued()
    3. PublierLe job entre dans la file sous l’identifiant trigger:<path>:<delivery>. serveTriggers()
    (Étapes)
    • → Worker : puis
  • WorkerExécute le job dans son propre processus.
    1. ExécuterUn workflow avec checkpoint, sous le runId du job. defineWorkflowJob()
    (Étapes)

Un job nomme un handler enregistré par le worker, un runId de 256 caractères au plus et un input JSON facultatif. Gardez on() rapide : GitHub attend une réponse pendant 10 secondes, Slack pendant 3 secondes.

SourceVérificationIdentifiant de livraisonevent.actor
createGithubWebhook({ secret })X-Hub-Signature-256, un HMAC du corps. Charges JSON ou formulaire.X-GitHub-Deliverygithub:<login>
createGitlabWebhook({ signingToken })webhook-signature avec un jeton de signature whsec_ (GitLab 19.0+), fenêtre de 5 minutes.webhook-idgitlab:<username>
createGitlabWebhook({ token })X-Gitlab-Token égal au jeton. Le corps n’est pas signé.Idempotency-Key, sinon X-Gitlab-Event-UUIDgitlab:<username>
createSlackSource({ signingSecret })X-Slack-Signature sur l’horodatage et le corps, fenêtre de 5 minutes.trigger_idslack:<user id>
createStandardWebhook({ secret })Secret whsec_ Standard Webhooks, fenêtre de 5 minutes.webhook-idaucun

Préférez un jeton de signature GitLab : un jeton simple circule tel quel dans un en-tête et ne signe pas le corps. toleranceMs modifie la fenêtre de 5 minutes. Les sources Slack acceptent les commandes slash et les charges interactives.

Deux fonctions utilitaires reconnaissent les événements courants et renvoient undefined pour tout le reste.

Référence API : labelAdded, commandIssued et TriggerEvent.

Consultez le contrat de l’événement pour traiter un autre type de livraison.

Référence API : TriggerEvent.

Une signature vérifiée prouve que la requête vient de votre intégration GitHub, GitLab ou Slack, pas que son auteur a le droit de lancer un workflow. Toute personne qui peut commenter un dépôt public peut écrire /outpost : comparez event.actor à une liste explicite.

import { commandIssued } from "@elie-laloum/outpost";
import type { TriggerEvent, TriggerJob } from "@elie-laloum/outpost";

const maintainers = new Set(["github:octocat", "slack:U012AB3CD"]);

function fromCommand(event: TriggerEvent): TriggerJob | undefined {
  const command = commandIssued(event, "/outpost");
  if (!command) return undefined;
  if (!event.actor || !maintainers.has(event.actor)) return undefined;
  return {
    handler: "fix",
    runId: `${command.repository ?? "slack"}#${command.number ?? event.delivery}`,
    input: { request: command.text },
  };
}

Passez fromCommand comme on d’une route. event.actor n’est pas un acteur d’étape d’approbation Outpost : les approbations authentifient leurs décideurs séparément.

StatutSignification
202Job publié, ou déjà publié pour cette livraison. Le corps vaut {"job": "<id>"}.
204Événement vérifié ignoré : on() a renvoyé undefined.
400Le corps de la requête n’a pas pu être lu.
401Vérification échouée : signature, secret, fenêtre d’horodatage ou en-tête manquant.
404Aucune route pour ce chemin.
405Méthode autre que POST.
413Corps au-delà de maxBytes : 1 Mio par défaut, 25 Mio au plus.
500on() a levé une erreur ou renvoyé un job invalide.
503La file a refusé le job. L’émetteur peut renvoyer la même livraison.

Les routes Slack répondent 200 avec un corps vide au lieu de 202 et 204. onError reçoit le path, l’étape stage (verify, route ou enqueue) et la delivery de l’échec, jamais un secret.

L’identifiant du job contient l’identifiant de livraison. Une nouvelle tentative de l’émetteur ou une relivraison manuelle le réutilise : la file garde un seul job tant qu’elle le conserve.

Une nouvelle livraison publie un nouveau job, même pour le même événement, comme un label ajouté à nouveau. C’est le runId qui décide si le travail est refait. Les traitements qui publient un résultat ont toujours besoin de leurs propres clés d’idempotence.

Identifier l’exécution à partir de l’événement

Section intitulée « Identifier l’exécution à partir de l’événement »

Construisez le runId à partir de ce qui identifie le travail dans la charge : owner/name#12, ou un commit de tête. Les jobs de même runId et de même input partagent un checkpoint : defineWorkflowJob() restaure les tâches déjà terminées au lieu de les relancer.

Un input différent sous le même runId échoue sur un checkpoint incompatible, car la version du checkpoint inclut une empreinte de l’input. Deux commandes au texte différent sur une même issue ont donc besoin de runId distincts, par exemple en y ajoutant event.delivery.

Les signatures GitHub ne portent pas d’horodatage : une requête interceptée peut être rejouée sous un nouvel identifiant de livraison. Un runId dérivé de la charge fait converger ce rejeu vers la même exécution. La recette Relire une pull request à la demande associe chaque exécution au commit de tête.

Chaque option de secret accepte aussi une fonction qui renvoie les secrets acceptés à cet instant. La source l’appelle à chaque requête.

import { createGithubWebhook } from "@elie-laloum/outpost";

const source = createGithubWebhook({
  secret: () =>
    [
      process.env.GITHUB_WEBHOOK_SECRET,
      process.env.GITHUB_WEBHOOK_SECRET_PREVIOUS,
    ].filter((value) => value !== undefined),
});

Acceptez les deux secrets, changez le secret chez l’émetteur, puis retirez l’ancien. Une fonction qui lève une erreur ou ne renvoie aucun secret refuse toutes les requêtes avec 401.

serveTriggers() écoute par défaut sur 127.0.0.1 ; host et port le modifient. Placez devant lui un reverse proxy qui termine TLS, et n’exposez que les chemins des routes.

import type { DurableTaskQueue, TriggerServer } from "@elie-laloum/outpost";

declare const server: TriggerServer;
declare const queue: DurableTaskQueue;

process.once("SIGTERM", async () => {
  await server.close();
  queue.close();
});

Fermez d’abord le serveur, pour qu’aucune requête n’atteigne une file fermée.

  • Outpost n’appelle pas les API GitHub, GitLab ou Slack : votre workflow publie les commentaires ou messages sur le résultat.
  • L’Events API de Slack et son défi de vérification d’URL ne sont pas pris en charge.
  • Aucune commande CLI outpost ne lance le serveur : démarrez-le depuis votre propre script.

API : serveTriggers · createGithubWebhook · createGitlabWebhook · createSlackSource · createStandardWebhook · labelAdded · commandIssued · TriggerEvent · TriggerJob · defineWorkflowJob.