tenseleyflow/shithub / 2592854

Browse files

actions/event: typed payload constructors per trigger (S41a-L4)

The shithub.event v1 schema was previously documentation-only —
docs/internal/actions-schema.md pinned the field layout but no
code enforced it. Audit S41a-L4 flagged this before S41b's trigger
pipeline writes the first event_payload row, since once data is in
the jsonb column the schema is sticky.

New package internal/actions/event with one constructor per trigger:
- event.Push(ref, before, after, hc HeadCommit)
- event.PullRequest(action, number, title, head, base, userLogin)
- event.Schedule()
- event.WorkflowDispatch(inputs map[string]string)

Each takes typed inputs (so adding a field is visible in the
function signature, reviewer-required by repo norms) and returns
map[string]any (what expr.evalEventPath consumes; also pgx-encodable
to jsonb without further transformation, so S41b passes the result
straight into InsertWorkflowRun).

The closed-door discipline matches the expression evaluator's
namespace + function allowlists: adding a field requires editing
the constructor, the doc, AND the corresponding *_FlowsThroughEvaluator
test in the same PR.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
25928549d91893a8f32483cc390d0336da694844
Parents
9c71d87
Tree
b752b89

1 changed file

StatusFile+-
A internal/actions/event/event.go 111 0
internal/actions/event/event.goadded
@@ -0,0 +1,111 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+// Package event builds canonical `shithub.event` payloads from
4
+// triggering domain events.
5
+//
6
+// The payload schema is the v1 contract documented in
7
+// docs/internal/actions-schema.md. Workflow authors template against
8
+// these field paths from `${{ shithub.event.X }}` (and the `github.event`
9
+// alias) — once data is in `workflow_runs.event_payload`, schema
10
+// changes are breaking. **Adding a field requires a reviewer-required
11
+// note + a doc update.** Treat this package the same way we treat
12
+// migration files.
13
+//
14
+// Each constructor takes typed inputs (so adding a field is visible
15
+// in the function signature) and returns a `map[string]any` because
16
+// that's exactly what `internal/actions/expr.evalEventPath` consumes.
17
+// The returned map is also pgx-encodable to jsonb without further
18
+// transformation, so callers (S41b's trigger pipeline) can pass it
19
+// straight into `InsertWorkflowRun`.
20
+//
21
+// What's NOT here in v1: `workflow_run`-event payloads (re-runs),
22
+// `release` events, `deployment_status`, anything outside the four
23
+// triggers documented in actions-schema.md. Those land in v2 with
24
+// their own constructors.
25
+package event
26
+
27
+// HeadCommit is the typed input shape for push-event head_commit.
28
+// Fields mirror the documented v1 schema: message, id, author.
29
+type HeadCommit struct {
30
+	Message string
31
+	ID      string
32
+	Author  string
33
+}
34
+
35
+// Push builds the payload for a push trigger.
36
+//
37
+// Documented v1 keys: ref, before, after, head_commit{message,id,author}.
38
+// The author field is a flat string (commit author name) in v1; if a
39
+// future field needs the full author object, that's a v2 break.
40
+func Push(ref, before, after string, hc HeadCommit) map[string]any {
41
+	return map[string]any{
42
+		"ref":    ref,
43
+		"before": before,
44
+		"after":  after,
45
+		"head_commit": map[string]any{
46
+			"message": hc.Message,
47
+			"id":      hc.ID,
48
+			"author":  hc.Author,
49
+		},
50
+	}
51
+}
52
+
53
+// PRRef is the head/base ref descriptor inside a pull_request payload.
54
+type PRRef struct {
55
+	Ref string // e.g. "feature/foo" or "main"
56
+	SHA string // 40-char object id
57
+}
58
+
59
+// PullRequest builds the payload for a pull_request trigger.
60
+//
61
+// Documented v1 keys:
62
+//
63
+//	action, number,
64
+//	pull_request{title, head{ref,sha}, base{ref,sha}, user{login}}.
65
+//
66
+// The action field tracks the activity type ("opened", "synchronize",
67
+// "reopened", "closed", "edited", "labeled", "unlabeled"). v1 doesn't
68
+// constrain the value here — S41b's pull_request emitter is the
69
+// allowlist.
70
+func PullRequest(action string, number int64, title string, head, base PRRef, userLogin string) map[string]any {
71
+	return map[string]any{
72
+		"action": action,
73
+		"number": number,
74
+		"pull_request": map[string]any{
75
+			"title": title,
76
+			"head": map[string]any{
77
+				"ref": head.Ref,
78
+				"sha": head.SHA,
79
+			},
80
+			"base": map[string]any{
81
+				"ref": base.Ref,
82
+				"sha": base.SHA,
83
+			},
84
+			"user": map[string]any{
85
+				"login": userLogin,
86
+			},
87
+		},
88
+	}
89
+}
90
+
91
+// Schedule is the payload for a cron-fired schedule trigger. Empty
92
+// by design — the cron expression itself is on the workflow_runs row,
93
+// not in the payload. Returned as a non-nil empty map so calling code
94
+// can pgx-encode without a nil check.
95
+func Schedule() map[string]any {
96
+	return map[string]any{}
97
+}
98
+
99
+// WorkflowDispatch builds the payload for a manual workflow_dispatch
100
+// trigger. Inputs are stringified (matching GHA semantics — even
101
+// boolean inputs arrive as "true"/"false" strings) and wrapped under
102
+// the `inputs` key so authors template `${{ shithub.event.inputs.foo }}`.
103
+func WorkflowDispatch(inputs map[string]string) map[string]any {
104
+	bag := make(map[string]any, len(inputs))
105
+	for k, v := range inputs {
106
+		bag[k] = v
107
+	}
108
+	return map[string]any{
109
+		"inputs": bag,
110
+	}
111
+}