Monorepo for Tangled
tangled.org
1package xrpc
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "net/http"
9
10 "github.com/bluesky-social/indigo/atproto/syntax"
11
12 "tangled.org/core/api/tangled"
13 "tangled.org/core/rbac"
14 xrpcerr "tangled.org/core/xrpc/errors"
15)
16
17func (x *Xrpc) TriggerPipeline(w http.ResponseWriter, r *http.Request) {
18 l := x.Logger
19 fail := func(e xrpcerr.XrpcError) {
20 l.Error("failed", "kind", e.Tag, "error", e.Message)
21 writeError(w, e, http.StatusBadRequest)
22 }
23 l.Debug("trigger pipeline")
24
25 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
26 if !ok {
27 fail(xrpcerr.MissingActorDidError)
28 return
29 }
30
31 var input tangled.CiTriggerPipeline_Input
32 if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
33 fail(xrpcerr.GenericError(err))
34 return
35 }
36
37 repoDid, xerr, ok := x.resolveOwnedRepo(r.Context(), actorDid, input.Repo)
38 if !ok {
39 fail(xerr)
40 return
41 }
42
43 var sha string
44 ref := ""
45 var sourceRepo syntax.DID
46 var pull PullContext
47 var inputs []*tangled.Pipeline_Pair
48
49 switch {
50 case input.Trigger == nil:
51 fail(xrpcerr.GenericError(fmt.Errorf("trigger is required")))
52 return
53
54 case input.Trigger.CiTrigger_Manual != nil:
55 manual := input.Trigger.CiTrigger_Manual
56 sha = manual.Sha
57 if manual.Ref != nil {
58 ref = *manual.Ref
59 }
60 parsed, err := parseOptionalDID("sourceRepo", manual.SourceRepo)
61 if err != nil {
62 fail(xrpcerr.GenericError(err))
63 return
64 }
65 sourceRepo = parsed
66 inputs = ciTriggerPairsToPipelinePairs(manual.Inputs)
67
68 case input.Trigger.CiTrigger_PullRequest != nil:
69 pr := input.Trigger.CiTrigger_PullRequest
70 sha = pr.SourceSha
71 parsed, err := parseOptionalDID("sourceRepo", pr.SourceRepo)
72 if err != nil {
73 fail(xrpcerr.GenericError(err))
74 return
75 }
76 sourceRepo = parsed
77
78 if pr.TargetBranch == "" {
79 fail(xrpcerr.GenericError(fmt.Errorf("pull request trigger targetBranch is required")))
80 return
81 }
82
83 var pullAt syntax.ATURI
84 if pr.Pull != nil {
85 var err error
86 pullAt, err = syntax.ParseATURI(*pr.Pull)
87 if err != nil {
88 fail(xrpcerr.InvalidRepoError(*pr.Pull))
89 return
90 }
91 }
92 sourceBranch := ""
93 if pr.SourceBranch != nil {
94 sourceBranch = *pr.SourceBranch
95 }
96 pull = PullContext{
97 IsPullRequest: true,
98 Pull: pullAt,
99 SourceBranch: sourceBranch,
100 TargetBranch: pr.TargetBranch,
101 }
102
103 default:
104 fail(xrpcerr.GenericError(fmt.Errorf("unsupported trigger variant")))
105 return
106 }
107
108 if len(sha) != 40 {
109 fail(xrpcerr.GenericError(fmt.Errorf("sha must be a 40-character commit hash")))
110 return
111 }
112
113 pipelineAt, err := x.Trigger.TriggerManual(r.Context(), repoDid, sha, ref, input.Workflows, sourceRepo, pull, inputs)
114 if errors.Is(err, ErrNoMatchingWorkflows) {
115 fail(xrpcerr.GenericError(err))
116 return
117 }
118 if err != nil {
119 fail(xrpcerr.GenericError(fmt.Errorf("failed to trigger pipeline: %w", err)))
120 return
121 }
122
123 if err := writeJson(w, http.StatusOK, tangled.CiTriggerPipeline_Output{
124 Pipeline: pipelineAt.String(),
125 }); err != nil {
126 l.Error("failed to write response", "err", err)
127 }
128}
129
130func parseOptionalDID(field string, value *string) (syntax.DID, error) {
131 if value == nil || *value == "" {
132 return "", nil
133 }
134 did, err := syntax.ParseDID(*value)
135 if err != nil {
136 return "", fmt.Errorf("invalid %s DID %q: %w", field, *value, err)
137 }
138 return did, nil
139}
140
141func ciTriggerPairsToPipelinePairs(inputs []*tangled.CiTrigger_Pair) []*tangled.Pipeline_Pair {
142 if len(inputs) == 0 {
143 return nil
144 }
145 pairs := make([]*tangled.Pipeline_Pair, 0, len(inputs))
146 for _, input := range inputs {
147 if input == nil {
148 continue
149 }
150 pairs = append(pairs, &tangled.Pipeline_Pair{
151 Key: input.Key,
152 Value: input.Value,
153 })
154 }
155 return pairs
156}
157
158// resolveOwnedRepo resolves a repository DID and checks push auth.
159func (x *Xrpc) resolveOwnedRepo(ctx context.Context, actorDid syntax.DID, repoDidStr string) (syntax.DID, xrpcerr.XrpcError, bool) {
160 repoDid, xerr, ok := x.resolveKnownRepoDid(repoDidStr)
161 if !ok {
162 return "", xerr, false
163 }
164
165 isPushAllowed, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid.String())
166 if err != nil || !isPushAllowed {
167 return "", xrpcerr.AccessControlError(actorDid.String()), false
168 }
169
170 return repoDid, xrpcerr.XrpcError{}, true
171}
172
173func (x *Xrpc) resolveKnownRepoDid(repoDidStr string) (syntax.DID, xrpcerr.XrpcError, bool) {
174 repoDid, err := syntax.ParseDID(repoDidStr)
175 if err != nil {
176 return "", xrpcerr.GenericError(fmt.Errorf("invalid repo DID %q: %w", repoDidStr, err)), false
177 }
178
179 if _, err := x.Db.GetRepoByDid(repoDid); err != nil {
180 return "", xrpcerr.RepoNotFoundError, false
181 }
182
183 return repoDid, xrpcerr.XrpcError{}, true
184}