This repository has no description
0

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 183 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 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}