go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luci_notify/cmd/preview_email/main.go (about)

     1  // Copyright 2019 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Command preview_email renders an email template file.
    16  package main
    17  
    18  import (
    19  	"context"
    20  	"flag"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  
    26  	"github.com/golang/protobuf/jsonpb"
    27  
    28  	"go.chromium.org/luci/buildbucket/cli"
    29  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    30  	"go.chromium.org/luci/common/data/text"
    31  	"go.chromium.org/luci/common/errors"
    32  	"go.chromium.org/luci/hardcoded/chromeinfra"
    33  
    34  	"go.chromium.org/luci/luci_notify/api/config"
    35  	"go.chromium.org/luci/luci_notify/mailtmpl"
    36  )
    37  
    38  type parsedFlags struct {
    39  	TemplateRootDir     string
    40  	BuildbucketHostname string
    41  	OldStatus           buildbucketpb.Status
    42  }
    43  
    44  func main() {
    45  	ctx := context.Background()
    46  
    47  	f := parsedFlags{
    48  		OldStatus: buildbucketpb.Status_SUCCESS,
    49  	}
    50  
    51  	flag.StringVar(&f.TemplateRootDir, "template-root-dir", "", text.Doc(`
    52  		Path to the email template dir.
    53  		Defaults to the parent directory of the template file
    54  	`))
    55  	flag.Var(cli.StatusFlag(&f.OldStatus), "old-status", text.Doc(`
    56  		Previous status of the builder.
    57  	`))
    58  	flag.StringVar(&f.BuildbucketHostname, "buildbucket-hostname", chromeinfra.BuildbucketHost, "Buildbucket hostname")
    59  
    60  	flag.Usage = func() {
    61  		fmt.Fprintf(flag.CommandLine.Output(), text.Doc(`
    62  			Usage: preview_email TEMPLATE_FILE [BUILD]
    63  
    64  			BUILD is a path to a buildbucket.v2.Build JSON file
    65  			https://chromium.googlesource.com/infra/luci/luci-go/+/HEAD/buildbucket/proto/build.proto
    66  			If not provided, reads the build JSON from stdin.
    67  
    68  			TEMPLATE_FILE is a path to a email template file.
    69  
    70  			Example: fetch a live build using bb tool and render an email for it
    71  				bb get -json -A 8914184822697034512 | preview_email ./default.template
    72  		`))
    73  		flag.PrintDefaults()
    74  	}
    75  
    76  	flag.Parse()
    77  
    78  	var buildPath, templatePath string
    79  	switch len(flag.Args()) {
    80  	case 2:
    81  		buildPath = flag.Arg(1)
    82  		fallthrough
    83  	case 1:
    84  		templatePath = flag.Arg(0)
    85  	default:
    86  		flag.Usage()
    87  		os.Exit(1)
    88  	}
    89  
    90  	if err := run(ctx, templatePath, buildPath, f); err != nil {
    91  		fmt.Fprintln(os.Stderr, err)
    92  		os.Exit(1)
    93  	}
    94  }
    95  
    96  func run(ctx context.Context, templateFile, buildPath string, f parsedFlags) error {
    97  	build, err := readBuild(buildPath)
    98  	if err != nil {
    99  		return errors.Annotate(err, "failed to read build").Err()
   100  	}
   101  
   102  	if templateFile, err = filepath.Abs(templateFile); err != nil {
   103  		return err
   104  	}
   105  	if _, err := os.Stat(templateFile); err != nil {
   106  		return err
   107  	}
   108  
   109  	if f.TemplateRootDir == "" {
   110  		f.TemplateRootDir = filepath.Dir(templateFile)
   111  	} else if f.TemplateRootDir, err = filepath.Abs(f.TemplateRootDir); err != nil {
   112  		return err
   113  	}
   114  
   115  	bundle := readTemplateBundle(ctx, f.TemplateRootDir)
   116  	templateName := templateName(templateFile, f.TemplateRootDir)
   117  	subject, body := bundle.GenerateEmail(templateName, &config.TemplateInput{
   118  		BuildbucketHostname: f.BuildbucketHostname,
   119  		Build:               build,
   120  		OldStatus:           f.OldStatus,
   121  	})
   122  
   123  	fmt.Println(subject)
   124  	fmt.Println()
   125  	fmt.Println(body)
   126  	return nil
   127  }
   128  
   129  func readBuild(buildPath string) (*buildbucketpb.Build, error) {
   130  	var f *os.File
   131  	if buildPath == "" {
   132  		f = os.Stdin
   133  	} else {
   134  		var err error
   135  		f, err = os.Open(buildPath)
   136  		if err != nil {
   137  			return nil, err
   138  		}
   139  		defer f.Close()
   140  	}
   141  
   142  	build := &buildbucketpb.Build{}
   143  	return build, jsonpb.Unmarshal(f, build)
   144  }
   145  
   146  func readTemplateBundle(ctx context.Context, templateRootDir string) *mailtmpl.Bundle {
   147  	templateRootDir, err := filepath.Abs(templateRootDir)
   148  	if err != nil {
   149  		return &mailtmpl.Bundle{Err: err}
   150  	}
   151  
   152  	var templates []*mailtmpl.Template
   153  	err = filepath.Walk(templateRootDir, func(path string, info os.FileInfo, err error) error {
   154  		if err != nil || info.IsDir() || !strings.HasSuffix(path, mailtmpl.FileExt) {
   155  			return err
   156  		}
   157  
   158  		t := &mailtmpl.Template{
   159  			Name: templateName(path, templateRootDir),
   160  			// Note: path is absolute.
   161  			DefinitionURL: "file://" + filepath.ToSlash(path),
   162  		}
   163  
   164  		contents, err := os.ReadFile(path)
   165  		if err != nil {
   166  			return errors.Annotate(err, "failed to read %q", path).Err()
   167  		}
   168  
   169  		t.SubjectTextTemplate, t.BodyHTMLTemplate, err = mailtmpl.SplitTemplateFile(string(contents))
   170  		if err != nil {
   171  			return errors.Annotate(err, "failed to parse %q", path).Err()
   172  		}
   173  
   174  		templates = append(templates, t)
   175  		return nil
   176  	})
   177  
   178  	b := mailtmpl.NewBundle(templates)
   179  	if b.Err == nil {
   180  		b.Err = err
   181  	}
   182  	return b
   183  }
   184  
   185  func templateName(templateFile, templateRootDir string) string {
   186  	templateFile = filepath.ToSlash(strings.TrimPrefix(templateFile, templateRootDir))
   187  	templateFile = strings.TrimPrefix(templateFile, "/")
   188  	return strings.TrimSuffix(templateFile, mailtmpl.FileExt)
   189  }