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