This repository has no description
0

Configure Feed

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

core / appview / pulls / trigger_ci.go
4.3 kB 156 lines
1package pulls 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "strings" 10 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 "tangled.org/core/api/tangled" 13 "tangled.org/core/appview/db" 14 "tangled.org/core/appview/models" 15 gitmirrorv1 "tangled.org/core/gitmirror/proto/gen" 16 "tangled.org/core/workflow" 17) 18 19func (s *Pulls) changedWorkflowFiles(ctx context.Context, baseRepo syntax.DID, base string, headRepo syntax.DID, head string) ([]string, error) { 20 req := &gitmirrorv1.DiffRequest{ 21 Head: &gitmirrorv1.RepoCommit{Repo: headRepo.String(), Commit: []byte(head)}, 22 } 23 if base != "" { 24 req.Base = &gitmirrorv1.RepoCommit{Repo: baseRepo.String(), Commit: []byte(base)} 25 } 26 stream, err := s.gitmirror.Diff(ctx, req) 27 if err != nil { 28 return nil, fmt.Errorf("failed to diff: %w", err) 29 } 30 var changed []string 31 for { 32 fd, err := stream.Recv() 33 if errors.Is(err, io.EOF) { 34 break 35 } 36 if err != nil { 37 return nil, fmt.Errorf("failed to drain diff response: %w", err) 38 } 39 for _, name := range []string{fd.LhsSrc.Path, fd.RhsSrc.Path} { 40 if name != "" && strings.HasPrefix(name, workflow.WorkflowDir+"/") { 41 changed = append(changed, name) 42 break 43 } 44 } 45 } 46 return changed, nil 47} 48 49// TriggerCi manually triggers a CI pipeline for a fork-based pull request. 50// authorized against and recorded under the target repo, but checked out 51// from the fork at the latest round's commit. 52func (s *Pulls) TriggerCi(w http.ResponseWriter, r *http.Request) { 53 l := s.logger.With("handler", "TriggerCi") 54 errorId := "pull-error" 55 56 fail := func(msg string, err error) { 57 if err != nil { 58 l.Error(msg, "err", err) 59 } else { 60 l.Error(msg) 61 } 62 s.pages.Notice(w, errorId, msg) 63 } 64 65 f, err := s.repoResolver.Resolve(r) 66 if err != nil { 67 fail("failed to resolve repository", err) 68 return 69 } 70 71 pull, ok := r.Context().Value("pull").(*models.Pull) 72 if !ok { 73 fail("failed to get pull", nil) 74 return 75 } 76 l = l.With("pull_id", pull.PullId) 77 78 if !pull.IsForkBased() { 79 fail("this pull request is not fork-based", nil) 80 return 81 } 82 83 if f.Spindle == "" { 84 fail("this repository has no spindle configured", nil) 85 return 86 } 87 88 latest := pull.LatestVersion() 89 if latest.Base == "" || latest.Head == "" { 90 fail("cannot trigger ci: this round has no commit to run", nil) 91 return 92 } 93 94 changedFiles, err := func(ctx context.Context) ([]string, error) { 95 base, err := s.resolveRev(ctx, pull.RepoDid, pull.TargetBranch) 96 if err != nil { 97 return nil, fmt.Errorf("failed to resolve target branch: %w", err) 98 } 99 return s.changedWorkflowFiles(ctx, pull.RepoDid, base, pull.SourceRepo, latest.Head) 100 }(r.Context()) 101 if err != nil { 102 fail("failed to inspect the latest round's patch", err) 103 return 104 } 105 if len(changedFiles) > 0 && r.URL.Query().Get("confirm") != "1" { 106 fail(fmt.Sprintf("workflow files changed in this round (%s); review before running", strings.Join(changedFiles, ", ")), nil) 107 return 108 } 109 110 forkRepo, err := db.GetRepoByDid(s.db, pull.SourceRepo.String()) 111 if err != nil { 112 fail("failed to resolve the fork this pull request comes from", err) 113 return 114 } 115 116 spindleClient, err := s.oauth.SpindleServiceClient(r, f.Spindle, tangled.CiTriggerPipelineNSID) 117 if err != nil { 118 fail("failed to authorize with spindle", err) 119 return 120 } 121 122 pullAt := pull.AtUri().String() 123 sourceBranch := pull.SourceBranch 124 targetBranch := pull.TargetBranch 125 out, err := tangled.CiTriggerPipeline( 126 r.Context(), 127 spindleClient, 128 &tangled.CiTriggerPipeline_Input{ 129 Repo: f.RepoDid, 130 Trigger: &tangled.CiTriggerPipeline_Input_Trigger{ 131 CiTrigger_PullRequest: &tangled.CiTrigger_PullRequest{ 132 Pull: &pullAt, 133 SourceBranch: sourceBranch, 134 SourceRepo: &forkRepo.RepoDid, 135 SourceSha: latest.Head, 136 TargetBranch: targetBranch, 137 }, 138 }, 139 }, 140 ) 141 if err != nil { 142 fail("spindle rejected the trigger", err) 143 return 144 } 145 l.Info("triggered ci for fork-based pull", "pipeline", out.Pipeline) 146 147 user := s.oauth.GetMultiAccountUser(r) 148 repoInfo := s.repoResolver.GetRepoInfo(r, user) 149 dest := fmt.Sprintf("/%s/pulls/%d/round/%d", repoInfo.FullName(), pull.PullId, pull.LatestVersionNumber()) 150 if r.Header.Get("HX-Request") == "true" { 151 w.Header().Set("HX-Redirect", dest) 152 w.WriteHeader(http.StatusOK) 153 return 154 } 155 http.Redirect(w, r, dest, http.StatusSeeOther) 156}