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 }