Monorepo for Tangled tangled.org
1

Configure Feed

Select the types of activity you want to include in your feed.

core / spindle / xrpc / ci_pipeline_trigger_pipeline.go
4.6 kB 184 lines
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}