@@ -27,6 +27,8 @@ import ( |
| 27 | 27 | "github.com/jackc/pgx/v5/pgtype" |
| 28 | 28 | "github.com/jackc/pgx/v5/pgxpool" |
| 29 | 29 | |
| 30 | + actionsevent "github.com/tenseleyFlow/shithub/internal/actions/event" |
| 31 | + "github.com/tenseleyFlow/shithub/internal/actions/trigger" |
| 30 | 32 | "github.com/tenseleyFlow/shithub/internal/auth/audit" |
| 31 | 33 | "github.com/tenseleyFlow/shithub/internal/checks" |
| 32 | 34 | "github.com/tenseleyFlow/shithub/internal/issues" |
@@ -37,6 +39,8 @@ import ( |
| 37 | 39 | repogit "github.com/tenseleyFlow/shithub/internal/repos/git" |
| 38 | 40 | "github.com/tenseleyFlow/shithub/internal/repos/protection" |
| 39 | 41 | reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc" |
| 42 | + usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc" |
| 43 | + "github.com/tenseleyFlow/shithub/internal/worker" |
| 40 | 44 | ) |
| 41 | 45 | |
| 42 | 46 | // 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 |
| 145 | 149 | } |
| 146 | 150 | } |
| 147 | 151 | |
| 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 | + |
| 148 | 164 | return CreateResult{Issue: issueRow, PullRequest: prRow}, nil |
| 149 | 165 | } |
| 150 | 166 | |
| 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 | + |
| 151 | 219 | // refreshCommitsAndFiles is shared by Create + Synchronize. Truncates + |
| 152 | 220 | // re-fills `pull_request_commits` and `pull_request_files`. |
| 153 | 221 | func refreshCommitsAndFiles(ctx context.Context, deps Deps, gitDir string, prID int64, baseOID, headOID string) error { |