This repository has no description
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}