golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cloudfns/sendwikidiff/sendwikidiff.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package sendwikidiff implements a Google Cloud background function that 6 // reacts to a pubsub message containing a GitHub webhook change payload. 7 // It assumes the payload is in reaction to a change to the Go wiki, then 8 // sends the full diff to the golang-wikichanges mailing list. 9 package sendwikidiff 10 11 import ( 12 "bytes" 13 "context" 14 "encoding/json" 15 "fmt" 16 "html/template" 17 "os" 18 "os/exec" 19 "path/filepath" 20 "strings" 21 "sync" 22 23 "github.com/sendgrid/sendgrid-go" 24 "github.com/sendgrid/sendgrid-go/helpers/mail" 25 ) 26 27 const repoURL = "https://github.com/golang/go.wiki.git" 28 29 var tempRepoDir = filepath.Join(os.TempDir(), strings.NewReplacer("://", "-", "/", "-").Replace(repoURL)) 30 31 var sendgridAPIKey = os.Getenv("SENDGRID_API_KEY") 32 33 type pubsubMessage struct { 34 Data []byte `json:"data"` 35 } 36 37 func HandleWikiChangePubSub(ctx context.Context, m pubsubMessage) error { 38 if sendgridAPIKey == "" { 39 return fmt.Errorf("Environment variable SENDGRID_API_KEY is empty") 40 } 41 42 var payload struct { 43 Pages []struct { 44 PageName string `json:"page_name"` 45 SHA string `json:"sha"` 46 } `json:"pages"` 47 } 48 if err := json.Unmarshal(m.Data, &payload); err != nil { 49 fmt.Fprintf(os.Stderr, "Unable to decode payload: %v", err) 50 return err 51 } 52 53 repo := newGitRepo(repoURL, tempRepoDir) 54 if err := repo.update(); err != nil { 55 fmt.Fprintf(os.Stderr, "Unable to update repo: %v", err) 56 return err 57 } 58 for _, page := range payload.Pages { 59 out, err := repo.cmdShow(page.SHA).Output() 60 if err != nil { 61 fmt.Fprintf(os.Stderr, "Could not show SHA %q: %v", page.SHA, err) 62 return err 63 } 64 if err := sendEmail(page.PageName, string(out)); err != nil { 65 fmt.Fprintf(os.Stderr, "Could not send email: %v", err) 66 return err 67 } 68 } 69 return nil 70 } 71 72 var htmlTmpl = template.Must(template.New("email").Parse(`<p><a href="{{.PageURL}}">View page</a></p> 73 <pre style="font-family: monospace,monospace; white-space: pre-wrap;">{{.Diff}}</pre> 74 `)) 75 76 func emailBody(page, diff string) (string, error) { 77 var buf bytes.Buffer 78 if err := htmlTmpl.Execute(&buf, struct { 79 PageURL, Diff string 80 }{ 81 Diff: diff, 82 PageURL: fmt.Sprintf("https://go.dev/wiki/%s", page), 83 }); err != nil { 84 return "", fmt.Errorf("template.Execute: %v", err) 85 } 86 return buf.String(), nil 87 } 88 89 func sendEmailSendGrid(page, diff string) error { 90 from := mail.NewEmail("WikiDiffBot", "nobody@golang.org") 91 subject := fmt.Sprintf("go.dev/wiki/%s was updated", page) 92 to := mail.NewEmail("", "golang-wikichanges@googlegroups.com") 93 94 body, err := emailBody(page, diff) 95 if err != nil { 96 return fmt.Errorf("emailBody: %v", err) 97 } 98 message := mail.NewSingleEmail(from, subject, to, diff, body) 99 client := sendgrid.NewSendClient(sendgridAPIKey) 100 _, err = client.Send(message) 101 return err 102 } 103 104 // sendEmail sends an email that the go.dev/wiki/$page was updated 105 // with the provided diff. 106 // Var for testing. 107 var sendEmail func(page, diff string) error = sendEmailSendGrid 108 109 type gitRepo struct { 110 sync.RWMutex 111 112 repo string // remote address of repo 113 dir string // location of the repo 114 } 115 116 func newGitRepo(repo, dir string) *gitRepo { 117 return &gitRepo{ 118 repo: repo, 119 dir: dir, 120 } 121 } 122 123 func (r *gitRepo) clone() error { 124 r.Lock() 125 defer r.Unlock() 126 cmd := exec.Command("git", "clone", r.repo, r.dir) 127 cmd.Stderr = os.Stderr 128 cmd.Stdout = os.Stdout 129 if err := cmd.Run(); err != nil { 130 return err 131 } 132 return nil 133 } 134 135 func (r *gitRepo) pull() error { 136 r.Lock() 137 defer r.Unlock() 138 cmd := exec.Command("git", "pull") 139 cmd.Dir = r.dir 140 cmd.Env = append(os.Environ(), "PWD="+r.dir) 141 cmd.Stderr = os.Stderr 142 cmd.Stdout = os.Stdout 143 if err := cmd.Run(); err != nil { 144 return err 145 } 146 return nil 147 } 148 149 func (r *gitRepo) update() error { 150 r.RLock() 151 _, err := os.Stat(r.dir) 152 r.RUnlock() 153 if os.IsNotExist(err) { 154 if err := r.clone(); err != nil { 155 return fmt.Errorf("could not clone %q into %q: %v", r.repo, r.dir, err) 156 } 157 return nil 158 } 159 160 if err := r.pull(); err != nil { 161 return fmt.Errorf("could not pull %q: %v", r.repo, err) 162 } 163 return nil 164 } 165 166 func (r *gitRepo) cmdShow(ref string) *exec.Cmd { 167 r.RLock() 168 defer r.RUnlock() 169 cmd := exec.Command("git", "show", ref) 170 cmd.Dir = r.dir 171 cmd.Env = append(os.Environ(), "PWD="+r.dir) 172 return cmd 173 }