forked from
tangled.org/core
Monorepo for Tangled
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}