Skip to content

Commit

Permalink
Add "--report file.md" flag to generate build report.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
meysholdt committed Nov 3, 2022
1 parent b5aebd8 commit f98c14e
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 2 deletions.
10 changes: 9 additions & 1 deletion cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ func init() {
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)")
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)")
buildCmd.Flags().Bool("watch", false, "Watch source files and re-build on change")
buildCmd.Flags().String("report", "", "Generate a markdown report after the build has finished. (e.g. --report myreport.md)")
}

func addBuildFlags(cmd *cobra.Command) {
Expand All @@ -172,7 +173,6 @@ func addBuildFlags(cmd *cobra.Command) {
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")
cmd.Flags().String("coverage-output-path", "", "Output path where test coverage file will be copied after running tests")
cmd.Flags().StringToString("docker-build-options", nil, "Options passed to all 'docker build' commands")

}

func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, *leeway.FilesystemCache) {
Expand Down Expand Up @@ -257,6 +257,14 @@ func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, *leeway.FilesystemC
reporter = leeway.NewConsoleReporter()
}

report, err := cmd.Flags().GetString("report")
if err != nil {
log.Fatal(err)
}
if report != "" {
reporter = leeway.NewMarkdownReporter(reporter, report)
}

dontTest, err := cmd.Flags().GetBool("dont-test")
if err != nil {
log.Fatal(err)
Expand Down
150 changes: 149 additions & 1 deletion pkg/leeway/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import (
"strings"
"sync"
"text/tabwriter"
"text/template"
"time"

log "github.com/sirupsen/logrus"

"github.com/gookit/color"
"github.com/segmentio/textio"
)
Expand All @@ -24,7 +27,7 @@ type Reporter interface {
// have been built.
BuildStarted(pkg *Package, status map[*Package]PackageBuildStatus)

// BuildFinished is called when the build of a package whcih was started by the user has finished.
// BuildFinished is called when the build of a package which was started by the user has finished.
// This is not the same as a dependency build finished (see PackageBuildFinished for that).
// The root package will also be passed into PackageBuildFinished once it's been built.
BuildFinished(pkg *Package, err error)
Expand Down Expand Up @@ -225,3 +228,148 @@ func (r *WerftReporter) PackageBuildFinished(pkg *Package, err error) {
}
fmt.Printf("[%s|%s] %s\n", pkg.FullName(), status, msg)
}

type PackageReport struct {
logs strings.Builder
start time.Time
duration time.Duration
status PackageBuildStatus
err error
}

func (r *PackageReport) StatusIcon() string {
if r.HasError() {
return "❌"
}
switch r.status {
case PackageBuilt:
return "✅"
case PackageBuilding:
return "🏃"
case PackageNotBuiltYet:
return "🔧"
default:
return "?"
}
}

func (r *PackageReport) HasError() bool {
return r.err != nil
}

func (r *PackageReport) DurationInSeconds() string {
return fmt.Sprintf("%.2fs", r.duration.Seconds())
}

func (r *PackageReport) Logs() string {
return strings.TrimSpace(r.logs.String())
}

func (r *PackageReport) Error() string {
return fmt.Sprintf("%s", r.err)
}

type MarkdownReporter struct {
delegate Reporter
filename string
reports map[string]*PackageReport
rootPackage *Package
mu sync.RWMutex
}

func NewMarkdownReporter(del Reporter, filename string) *MarkdownReporter {
return &MarkdownReporter{
delegate: del,
filename: filename,
reports: make(map[string]*PackageReport),
}
}

func (r *MarkdownReporter) getReport(pkg *Package) *PackageReport {
name := pkg.FullName()

r.mu.RLock()
rep, ok := r.reports[name]
r.mu.RUnlock()

if !ok {
r.mu.Lock()
rep, ok = r.reports[name]
if ok {
r.mu.Unlock()
return rep
}

rep = &PackageReport{status: PackageNotBuiltYet}
r.reports[name] = rep
r.mu.Unlock()
}

return rep
}

func (r *MarkdownReporter) BuildStarted(pkg *Package, status map[*Package]PackageBuildStatus) {
r.rootPackage = pkg
r.delegate.BuildStarted(pkg, status)
}

func (r *MarkdownReporter) BuildFinished(pkg *Package, err error) {
r.delegate.BuildFinished(pkg, err)
r.Report()
}

func (r *MarkdownReporter) PackageBuildStarted(pkg *Package) {
r.delegate.PackageBuildStarted(pkg)
rep := r.getReport(pkg)
rep.start = time.Now()
rep.status = PackageBuilding
}

func (r *MarkdownReporter) PackageBuildLog(pkg *Package, isErr bool, buf []byte) {
report := r.getReport(pkg)
report.logs.Write(buf)
r.delegate.PackageBuildLog(pkg, isErr, buf)
}

func (r *MarkdownReporter) PackageBuildFinished(pkg *Package, err error) {
r.delegate.PackageBuildFinished(pkg, err)
rep := r.getReport(pkg)
rep.duration = time.Since(rep.start)
rep.status = PackageBuilt
rep.err = err
}

func (r *MarkdownReporter) Report() {
vars := make(map[string]interface{})
vars["Name"] = r.filename
vars["Packages"] = r.reports
vars["RootPackage"] = r.rootPackage

tmplString := `
# Leeway build for '{{ .RootPackage.FullName }}'
{{ range $pkg, $report := .Packages }}
<details{{ if $report.HasError }} open{{ end }}><summary> {{ $report.StatusIcon }} <b>{{ $pkg }}</b> - {{ $report.DurationInSeconds }}</summary>
{{ if $report.HasError }}
<pre>
{{ $report.Error }}
</pre>
{{ end }}
<pre>
{{ $report.Logs }}
</pre>
</details>
{{ end }}
`
tmpl, _ := template.New("Report").Parse(strings.ReplaceAll(tmplString, "'", "`"))

file, _ := os.Create(r.filename)
defer file.Close()

err := tmpl.Execute(file, vars)
if err != nil {
log.WithError(err).Fatal("Can render template")
}
}

0 comments on commit f98c14e

Please sign in to comment.