tenseleyflow/shithub / b6464d9

Browse files

pulls.Create: enqueue workflow:trigger with action=opened (S41b)

PR-create-side trigger fan-out. Lives in the pulls orchestrator (not
a domain_event watcher) so the open path stays self-contained.

- trigger_event_id = &#39;pr_opened:<pr_id>:<head_sha>&#39; — distinct from
the synchronize key so the same SHA can produce both an &#39;opened&#39;
run AND a &#39;synchronize&#39; run if the workflow author listens for both
- same collaborator gate as the synchronize site (actor must be the
repo&#39;s owning user)
- changed_paths derived from base..head; best-effort on diff failure

Both PR sites use the same pattern: build canonical event payload via
internal/actions/event, populate filter hints (action, base_ref,
head_ref_short, changed_paths) on the worker payload, enqueue.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
b6464d9f6fabe1b66762fa727adee5bc6227976a
Parents
526dab9
Tree
e8c1069

1 changed file

StatusFile+-
M internal/pulls/pulls.go 68 0
internal/pulls/pulls.gomodified
@@ -27,6 +27,8 @@ import (
2727
 	"github.com/jackc/pgx/v5/pgtype"
2828
 	"github.com/jackc/pgx/v5/pgxpool"
2929
 
30
+	actionsevent "github.com/tenseleyFlow/shithub/internal/actions/event"
31
+	"github.com/tenseleyFlow/shithub/internal/actions/trigger"
3032
 	"github.com/tenseleyFlow/shithub/internal/auth/audit"
3133
 	"github.com/tenseleyFlow/shithub/internal/checks"
3234
 	"github.com/tenseleyFlow/shithub/internal/issues"
@@ -37,6 +39,8 @@ import (
3739
 	repogit "github.com/tenseleyFlow/shithub/internal/repos/git"
3840
 	"github.com/tenseleyFlow/shithub/internal/repos/protection"
3941
 	reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc"
42
+	usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc"
43
+	"github.com/tenseleyFlow/shithub/internal/worker"
4044
 )
4145
 
4246
 // Deps wires this package against the rest of the runtime.
@@ -145,9 +149,73 @@ func Create(ctx context.Context, deps Deps, p CreateParams) (CreateResult, error
145149
 		}
146150
 	}
147151
 
152
+	// Actions trigger (S41b): on PR open, fan out a workflow:trigger
153
+	// with action="opened". Best-effort — failures log and let the
154
+	// PR creation succeed. The collaborator gate lives in the PR
155
+	// trigger helper (pr_jobs.go); this site just enqueues with
156
+	// enough payload for the handler to evaluate.
157
+	if err := enqueueOpenedActionsTrigger(ctx, deps, p, prRow, issueRow.Number, baseOID, headOID); err != nil {
158
+		if deps.Logger != nil {
159
+			deps.Logger.WarnContext(ctx, "pulls: enqueue workflow:trigger",
160
+				"error", err, "pr_id", prRow.IssueID)
161
+		}
162
+	}
163
+
148164
 	return CreateResult{Issue: issueRow, PullRequest: prRow}, nil
149165
 }
150166
 
167
+// enqueueOpenedActionsTrigger is the PR-create-side counterpart to
168
+// jobs.enqueuePRActionsTrigger (which handles synchronize). Lives
169
+// here in the pulls orchestrator so the open path stays
170
+// self-contained — the alternative (a domain_event watcher in the
171
+// jobs package) would need to round-trip through the queue just to
172
+// observe the open.
173
+//
174
+// Collaborator gate: actor must be the repo's owning user. Same v1
175
+// posture as the synchronize path.
176
+func enqueueOpenedActionsTrigger(ctx context.Context, deps Deps, p CreateParams, prRow pullsdb.PullRequest, prNumber int64, baseOID, headOID string) error {
177
+	repo, err := reposdb.New().GetRepoByID(ctx, deps.Pool, p.RepoID)
178
+	if err != nil {
179
+		return fmt.Errorf("load repo: %w", err)
180
+	}
181
+	if !repo.OwnerUserID.Valid || repo.OwnerUserID.Int64 != p.AuthorUserID {
182
+		// Conservative collaborator gate — non-owner authors don't
183
+		// trigger. External-PR + org-member triggers parked for v2.
184
+		return nil
185
+	}
186
+	changed, err := repogit.ChangedPaths(ctx, p.GitDir, baseOID, headOID)
187
+	if err != nil {
188
+		changed = nil // best-effort; path-filtered workflows skip
189
+	}
190
+	authorLogin := ""
191
+	if u, err := usersdb.New().GetUserByID(ctx, deps.Pool, p.AuthorUserID); err == nil {
192
+		authorLogin = u.Username
193
+	}
194
+	payload := actionsevent.PullRequest(
195
+		"opened", prNumber, p.Title,
196
+		actionsevent.PRRef{Ref: prRow.HeadRef, SHA: prRow.HeadOid},
197
+		actionsevent.PRRef{Ref: prRow.BaseRef, SHA: prRow.BaseOid},
198
+		authorLogin,
199
+	)
200
+	job := trigger.JobPayload{
201
+		RepoID:         p.RepoID,
202
+		HeadSHA:        prRow.HeadOid,
203
+		HeadRef:        "refs/heads/" + prRow.HeadRef,
204
+		EventKind:      trigger.EventPullRequest,
205
+		EventPayload:   payload,
206
+		ActorUserID:    p.AuthorUserID,
207
+		TriggerEventID: fmt.Sprintf("pr_opened:%d:%s", prRow.IssueID, prRow.HeadOid),
208
+		Action:         "opened",
209
+		BaseRef:        prRow.BaseRef,
210
+		HeadRefShort:   prRow.HeadRef,
211
+		ChangedPaths:   changed,
212
+	}
213
+	if _, err := worker.Enqueue(ctx, deps.Pool, trigger.KindWorkflowTrigger, job, worker.EnqueueOptions{}); err != nil {
214
+		return fmt.Errorf("enqueue: %w", err)
215
+	}
216
+	return nil
217
+}
218
+
151219
 // refreshCommitsAndFiles is shared by Create + Synchronize. Truncates +
152220
 // re-fills `pull_request_commits` and `pull_request_files`.
153221
 func refreshCommitsAndFiles(ctx context.Context, deps Deps, gitDir string, prID int64, baseOID, headOID string) error {