Monorepo for Tangled
0

Configure Feed

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

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