tenseleyflow/shithub / b5d3edd

Browse files

docs: trigger pipeline section in actions-schema.md (S41b)

Documents the three-layer flow (caller → worker → enqueue), the
trigger_event_id idempotency convention with the per-caller
construction table, the per-event-kind match semantics + glob
subset, the conservative collaborator gate decision, and the
workflow_dispatch HTTP surface. Calls out the S41b/S41b-2 split
and what's deliberately out of scope until S41c+.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
b5d3edd8244b1b191ea1b8e2b77fd309d791d212
Parents
26489d3
Tree
90abfcf

1 changed file

StatusFile+-
M docs/internal/actions-schema.md 108 0
docs/internal/actions-schema.mdmodified
@@ -329,6 +329,114 @@ Other admin surfaces are scoped to later sub-sprints:
329329
 - S41g: `shithubd admin actions cancel <run-id>` flips
330330
   `cancel_requested`.
331331
 
332
+## Trigger pipeline (S41b)
333
+
334
+Three layers between a triggering event and a queued `workflow_run`:
335
+
336
+```
337
+caller (push_process / pulls.Create / pr_jobs.PRSynchronize / dispatch HTTP)
338
+    │
339
+    └─► worker.Enqueue(KindWorkflowTrigger, JobPayload)
340
+            │
341
+            └─► trigger.Handler picks up:
342
+                  Discover .shithub/workflows/*.yml at HEAD SHA
343
+                  Parse each (skip + log on Error diagnostics)
344
+                  Match each against trigger.Event
345
+                  Enqueue each match
346
+                        │
347
+                        └─► trigger.Enqueue (one tx):
348
+                              INSERT workflow_runs (ON CONFLICT DO NOTHING)
349
+                              INSERT workflow_jobs per parsed job
350
+                              INSERT workflow_steps per parsed step
351
+                              (commit)
352
+                              checks.Create per job (post-tx, idempotent
353
+                                via ExternalID 'workflow_run:<id>:job:<key>')
354
+```
355
+
356
+### Idempotency on the triggering event
357
+
358
+The robust pattern, not a UNIQUE on `(repo_id, head_sha)`. Each
359
+caller constructs a stable `trigger_event_id` from its triggering
360
+event's identity:
361
+
362
+| Caller              | trigger_event_id format                          |
363
+| ------------------- | ------------------------------------------------ |
364
+| push_process        | `push:<push_event_id>`                           |
365
+| pulls.Create        | `pr_opened:<pr_id>:<head_sha>`                   |
366
+| pr_jobs.PRSynchronize | `pr_synchronize:<pr_id>:<head_sha>`            |
367
+| dispatch HTTP       | `dispatch:<file>:<sha>:<8-byte-random-hex>`      |
368
+| schedule sweep (S41b-2) | `schedule:<workflow_id>:<window_start_unix>` |
369
+
370
+Migration 0051 adds `workflow_runs.trigger_event_id` (text NOT NULL
371
+DEFAULT '') with a partial UNIQUE on
372
+`(repo_id, workflow_file, trigger_event_id) WHERE trigger_event_id <> ''`.
373
+The trigger handler does `INSERT … ON CONFLICT DO NOTHING` so:
374
+
375
+- Worker retries (the same push_process replay) → no duplicate runs.
376
+- Admin replays via `shithubd admin run-job workflow:trigger ...`
377
+  → no duplicate runs.
378
+- Re-runs (the future "Re-run" button) explicitly construct a NEW
379
+  trigger_event_id (`rerun:<original_run_id>:<request_uuid>`) and
380
+  chain back via `parent_run_id`. History is preserved, no
381
+  collision.
382
+
383
+Each caller's collision-free namespace is short-lived and
384
+human-debuggable: a Postgres operator can grep
385
+`workflow_runs.trigger_event_id` to see exactly which triggering
386
+event produced a given run.
387
+
388
+### Filter evaluation
389
+
390
+`trigger.Match(workflow, event)` is a pure function (no I/O, no DB).
391
+For each event kind:
392
+
393
+- **push**: branch vs tag classified from the ref; only the matching
394
+  filter list applies (a `branches:` filter rejects tag pushes and
395
+  vice versa). `paths:` (when set) requires at least one changed
396
+  path to match. Empty filter = match-all.
397
+- **pull_request**: `types:` defaults to
398
+  `[opened, synchronize, reopened]` when omitted (GHA parity).
399
+  `branches:` applies to the **base** ref. `paths:` as for push.
400
+- **schedule**: requires the workflow to declare the cron expression
401
+  that fired. The sweep is the source of truth for which cron
402
+  fires; we just gate on declaration. Avoids interpreting cron
403
+  semantics in two places.
404
+- **workflow_dispatch**: matches whenever the workflow declares
405
+  `on.workflow_dispatch`.
406
+
407
+Glob semantics in `branches:`/`tags:`/`paths:`: minimatch subset
408
+with `*` (single segment), `**` (any), `/**` end-anchor (optional
409
+trailing path), `**/` start-anchor, and `!exclude` (last-match-wins,
410
+exclusion-only list implies include-all).
411
+
412
+### Collaborator gate
413
+
414
+Per the S41b spec's "external-PR support is parked" decision: PR
415
+triggers (both `opened` and `synchronize`) only fire when the PR's
416
+author is the repo's owning user. Conservative — drops legitimate
417
+non-owner collaborators in the org-repo case. Expanding the gate
418
+requires plumbing `policy.Can` into the worker context, which we
419
+defer to S41g where the lifecycle work touches that surface anyway.
420
+
421
+### Operator surface
422
+
423
+- `POST /{owner}/{repo}/actions/workflows/{file}/dispatches`
424
+  Body: `{"ref": "...", "inputs": {"key": "value"}}` (both optional;
425
+  ref defaults to the repo's default branch). Returns 204 No Content
426
+  on success. Synchronous trigger.Enqueue (no discovery — file is
427
+  named in the URL). Auth: requires repo write.
428
+
429
+### What S41b deliberately doesn't do
430
+
431
+- Run jobs. Runs sit in `queued` forever — S41c+ runner work.
432
+- Schedule sweep. Cron-driven triggers split into S41b-2 to keep
433
+  this PR reviewable; the trigger pipeline accepts schedule events,
434
+  but no caller produces them yet. S41b-2 adds the sweep + the
435
+  `robfig/cron/v3` dep + `shithubd-cron.service` wiring.
436
+- External-PR triggers. Conservative collaborator gate above.
437
+- `workflow_run` webhook events. S41h adds the webhook event family
438
+  + atom feed.
439
+
332440
 ## What S41a deliberately doesn't do
333441
 
334442
 - No trigger pipeline. `domain_events` aren't matched against `on:`