Skip to content

Commit b2e0dc4

Browse files
committed
Add "--report file.html" flag to generate build report.
The report is rendered in Markdown and useful when using Leeway in environments that doen't support log cutting. For example: * Gitpod Workspaces * CI systems other than Werft
1 parent 143e4b6 commit b2e0dc4

File tree

2 files changed

+159
-2
lines changed

2 files changed

+159
-2
lines changed

cmd/build.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ func init() {
154154
buildCmd.Flags().String("serve", "", "After a successful build this starts a webserver on the given address serving the build result (e.g. --serve localhost:8080)")
155155
buildCmd.Flags().String("save", "", "After a successful build this saves the build result as tar.gz file in the local filesystem (e.g. --save build-result.tar.gz)")
156156
buildCmd.Flags().Bool("watch", false, "Watch source files and re-build on change")
157+
157158
}
158159

159160
func addBuildFlags(cmd *cobra.Command) {
@@ -172,7 +173,7 @@ func addBuildFlags(cmd *cobra.Command) {
172173
cmd.Flags().UintP("max-concurrent-tasks", "j", uint(runtime.NumCPU()), "Limit the number of max concurrent build tasks - set to 0 to disable the limit")
173174
cmd.Flags().String("coverage-output-path", "", "Output path where test coverage file will be copied after running tests")
174175
cmd.Flags().StringToString("docker-build-options", nil, "Options passed to all 'docker build' commands")
175-
176+
cmd.Flags().String("report", "", "Generate a HTML report after the build has finished. (e.g. --report myreport.html)")
176177
}
177178

178179
func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, *leeway.FilesystemCache) {
@@ -257,6 +258,14 @@ func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, *leeway.FilesystemC
257258
reporter = leeway.NewConsoleReporter()
258259
}
259260

261+
report, err := cmd.Flags().GetString("report")
262+
if err != nil {
263+
log.Fatal(err)
264+
}
265+
if report != "" {
266+
reporter = leeway.NewHTMLReporter(reporter, report)
267+
}
268+
260269
dontTest, err := cmd.Flags().GetBool("dont-test")
261270
if err != nil {
262271
log.Fatal(err)

pkg/leeway/reporter.go

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import (
88
"strings"
99
"sync"
1010
"text/tabwriter"
11+
"text/template"
1112
"time"
1213

14+
log "github.com/sirupsen/logrus"
15+
1316
"github.com/gookit/color"
1417
"github.com/segmentio/textio"
1518
)
@@ -24,7 +27,7 @@ type Reporter interface {
2427
// have been built.
2528
BuildStarted(pkg *Package, status map[*Package]PackageBuildStatus)
2629

27-
// BuildFinished is called when the build of a package whcih was started by the user has finished.
30+
// BuildFinished is called when the build of a package which was started by the user has finished.
2831
// This is not the same as a dependency build finished (see PackageBuildFinished for that).
2932
// The root package will also be passed into PackageBuildFinished once it's been built.
3033
BuildFinished(pkg *Package, err error)
@@ -225,3 +228,148 @@ func (r *WerftReporter) PackageBuildFinished(pkg *Package, err error) {
225228
}
226229
fmt.Printf("[%s|%s] %s\n", pkg.FullName(), status, msg)
227230
}
231+
232+
type PackageReport struct {
233+
logs strings.Builder
234+
start time.Time
235+
duration time.Duration
236+
status PackageBuildStatus
237+
err error
238+
}
239+
240+
func (r *PackageReport) StatusIcon() string {
241+
if r.HasError() {
242+
return "❌"
243+
}
244+
switch r.status {
245+
case PackageBuilt:
246+
return "✅"
247+
case PackageBuilding:
248+
return "🏃"
249+
case PackageNotBuiltYet:
250+
return "🔧"
251+
default:
252+
return "?"
253+
}
254+
}
255+
256+
func (r *PackageReport) HasError() bool {
257+
return r.err != nil
258+
}
259+
260+
func (r *PackageReport) DurationInSeconds() string {
261+
return fmt.Sprintf("%.2fs", r.duration.Seconds())
262+
}
263+
264+
func (r *PackageReport) Logs() string {
265+
return strings.TrimSpace(r.logs.String())
266+
}
267+
268+
func (r *PackageReport) Error() string {
269+
return fmt.Sprintf("%s", r.err)
270+
}
271+
272+
type HTMLReporter struct {
273+
delegate Reporter
274+
filename string
275+
reports map[string]*PackageReport
276+
rootPackage *Package
277+
mu sync.RWMutex
278+
}
279+
280+
func NewHTMLReporter(del Reporter, filename string) *HTMLReporter {
281+
return &HTMLReporter{
282+
delegate: del,
283+
filename: filename,
284+
reports: make(map[string]*PackageReport),
285+
}
286+
}
287+
288+
func (r *HTMLReporter) getReport(pkg *Package) *PackageReport {
289+
name := pkg.FullName()
290+
291+
r.mu.RLock()
292+
rep, ok := r.reports[name]
293+
r.mu.RUnlock()
294+
295+
if !ok {
296+
r.mu.Lock()
297+
rep, ok = r.reports[name]
298+
if ok {
299+
r.mu.Unlock()
300+
return rep
301+
}
302+
303+
rep = &PackageReport{status: PackageNotBuiltYet}
304+
r.reports[name] = rep
305+
r.mu.Unlock()
306+
}
307+
308+
return rep
309+
}
310+
311+
func (r *HTMLReporter) BuildStarted(pkg *Package, status map[*Package]PackageBuildStatus) {
312+
r.rootPackage = pkg
313+
r.delegate.BuildStarted(pkg, status)
314+
}
315+
316+
func (r *HTMLReporter) BuildFinished(pkg *Package, err error) {
317+
r.delegate.BuildFinished(pkg, err)
318+
r.Report()
319+
}
320+
321+
func (r *HTMLReporter) PackageBuildStarted(pkg *Package) {
322+
r.delegate.PackageBuildStarted(pkg)
323+
rep := r.getReport(pkg)
324+
rep.start = time.Now()
325+
rep.status = PackageBuilding
326+
}
327+
328+
func (r *HTMLReporter) PackageBuildLog(pkg *Package, isErr bool, buf []byte) {
329+
report := r.getReport(pkg)
330+
report.logs.Write(buf)
331+
r.delegate.PackageBuildLog(pkg, isErr, buf)
332+
}
333+
334+
func (r *HTMLReporter) PackageBuildFinished(pkg *Package, err error) {
335+
r.delegate.PackageBuildFinished(pkg, err)
336+
rep := r.getReport(pkg)
337+
rep.duration = time.Since(rep.start)
338+
rep.status = PackageBuilt
339+
rep.err = err
340+
}
341+
342+
func (r *HTMLReporter) Report() {
343+
vars := make(map[string]interface{})
344+
vars["Name"] = r.filename
345+
vars["Packages"] = r.reports
346+
vars["RootPackage"] = r.rootPackage
347+
348+
tmplString := `
349+
<h1> Leeway build for <code>{{ .RootPackage.FullName }}</code></h1>
350+
{{ range $pkg, $report := .Packages }}
351+
<details{{ if $report.HasError }} open{{ end }}><summary> {{ $report.StatusIcon }} <b>{{ $pkg }}</b> - {{ $report.DurationInSeconds }}</summary>
352+
353+
{{ if $report.HasError }}
354+
<pre>
355+
{{ $report.Error }}
356+
</pre>
357+
{{ end }}
358+
359+
<pre>
360+
{{ $report.Logs }}
361+
</pre>
362+
363+
</details>
364+
{{ end }}
365+
`
366+
tmpl, _ := template.New("Report").Parse(strings.ReplaceAll(tmplString, "'", "`"))
367+
368+
file, _ := os.Create(r.filename)
369+
defer file.Close()
370+
371+
err := tmpl.Execute(file, vars)
372+
if err != nil {
373+
log.WithError(err).Fatal("Can't render template")
374+
}
375+
}

0 commit comments

Comments
 (0)