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  }