593 lines
18 KiB
Go
593 lines
18 KiB
Go
package workflow
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
"text/tabwriter"
|
|
"time"
|
|
)
|
|
|
|
func renderTriageReport(w io.Writer, report TriageReport, format string) error {
|
|
switch normalizeFormat(format) {
|
|
case "json":
|
|
return writeJSON(w, report)
|
|
case "markdown":
|
|
return writeTriageMarkdown(w, report)
|
|
case "table":
|
|
return writeTriageTable(w, report)
|
|
default:
|
|
return fmt.Errorf("unsupported workflow output format %q", format)
|
|
}
|
|
}
|
|
|
|
func renderHealthResult(w io.Writer, result HealthResult, format string) error {
|
|
switch normalizeFormat(format) {
|
|
case "json":
|
|
return writeJSON(w, result)
|
|
case "markdown":
|
|
return writeHealthMarkdown(w, result)
|
|
case "table":
|
|
return writeHealthTable(w, result)
|
|
default:
|
|
return fmt.Errorf("unsupported workflow output format %q", format)
|
|
}
|
|
}
|
|
|
|
func renderStaleReport(w io.Writer, report StaleReport, format string, lang string) error {
|
|
switch normalizeFormat(format) {
|
|
case "json":
|
|
return writeJSON(w, report)
|
|
case "markdown":
|
|
return writeStaleMarkdown(w, report, lang)
|
|
case "table":
|
|
return writeStaleTable(w, report)
|
|
default:
|
|
return fmt.Errorf("unsupported workflow output format %q", format)
|
|
}
|
|
}
|
|
|
|
func RenderPRSummary(result PRSummaryResult, format string, lang string) (string, error) {
|
|
var buf bytes.Buffer
|
|
switch normalizeFormat(format) {
|
|
case "json":
|
|
if err := writeJSON(&buf, result); err != nil {
|
|
return "", err
|
|
}
|
|
case "markdown":
|
|
if err := writePRSummaryMarkdown(&buf, result, lang); err != nil {
|
|
return "", err
|
|
}
|
|
case "table":
|
|
if err := writePRSummaryTable(&buf, result); err != nil {
|
|
return "", err
|
|
}
|
|
default:
|
|
return "", fmt.Errorf("unsupported workflow output format %q", format)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func RenderRepoReport(result RepoReportResult, format string, lang string) (string, error) {
|
|
var buf bytes.Buffer
|
|
switch normalizeFormat(format) {
|
|
case "json":
|
|
if err := writeJSON(&buf, result); err != nil {
|
|
return "", err
|
|
}
|
|
case "markdown":
|
|
if err := writeRepoReportMarkdown(&buf, result, lang); err != nil {
|
|
return "", err
|
|
}
|
|
case "table":
|
|
if err := writeRepoReportTable(&buf, result, lang); err != nil {
|
|
return "", err
|
|
}
|
|
default:
|
|
return "", fmt.Errorf("unsupported workflow output format %q", format)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func normalizeFormat(format string) string {
|
|
format = strings.ToLower(strings.TrimSpace(format))
|
|
if format == "" {
|
|
return "json"
|
|
}
|
|
return format
|
|
}
|
|
|
|
func writeJSON(w io.Writer, data interface{}) error {
|
|
encoded, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = fmt.Fprintln(w, string(encoded))
|
|
return err
|
|
}
|
|
|
|
func writeTriageTable(w io.Writer, report TriageReport) error {
|
|
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
|
|
if _, err := fmt.Fprintln(tw, "NUMBER\tTYPE\tPRIORITY\tCONFIDENCE\tMISSING\tACTION"); err != nil {
|
|
return err
|
|
}
|
|
for _, result := range report.Results {
|
|
missing := "-"
|
|
if len(result.MissingInformation) > 0 {
|
|
missing = strings.Join(result.MissingInformation, ",")
|
|
}
|
|
if _, err := fmt.Fprintf(tw, "%d\t%s\t%s\t%d\t%s\t%s\n",
|
|
result.Issue.Number,
|
|
result.DetectedType,
|
|
result.Priority,
|
|
result.Confidence,
|
|
missing,
|
|
result.RecommendedAction,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tw.Flush()
|
|
}
|
|
|
|
func writePRSummaryTable(w io.Writer, result PRSummaryResult) error {
|
|
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
|
|
if _, err := fmt.Fprintln(tw, "PR\tTITLE\tTYPE\tRISK\tFILES\tCOMMITS\tSOURCE"); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintf(tw, "#%d\t%s\t%s\t%s\t%d\t%d\t%s\n",
|
|
result.Number,
|
|
truncateTableText(result.Title, 72),
|
|
result.ChangeType,
|
|
result.RiskLevel,
|
|
result.ChangedFilesCount,
|
|
result.CommitCount,
|
|
result.Source,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
return tw.Flush()
|
|
}
|
|
|
|
func writeHealthTable(w io.Writer, result HealthResult) error {
|
|
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
|
|
if _, err := fmt.Fprintf(tw, "REPOSITORY\tSCORE\tRISK\n%s\t%d\t%s\n\n", result.Repository, result.HealthScore, result.RiskLevel); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(tw, "METRIC\tSTATUS\tSCORE\tMAX\tREASON"); err != nil {
|
|
return err
|
|
}
|
|
for _, metric := range result.Metrics {
|
|
if _, err := fmt.Fprintf(tw, "%s\t%s\t%d\t%d\t%s\n", metric.Name, metric.Status, metric.Score, metric.MaxScore, metric.Reason); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tw.Flush()
|
|
}
|
|
|
|
func writeRepoReportTable(w io.Writer, result RepoReportResult, lang string) error {
|
|
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
|
|
healthScore := repoReportText(lang, "not_available")
|
|
if result.Health != nil {
|
|
healthScore = fmt.Sprintf("%d", result.Health.HealthScore)
|
|
}
|
|
topRecommendation := repoReportText(lang, "not_available")
|
|
if len(result.Recommendations) > 0 {
|
|
topRecommendation = truncateTableText(result.Recommendations[0], 96)
|
|
}
|
|
openPRs, mergedPRs, closedPRs := "N/A", "N/A", "N/A"
|
|
if result.PRLifecycle != nil {
|
|
openPRs = fmt.Sprintf("%d", result.PRLifecycle.Open)
|
|
mergedPRs = fmt.Sprintf("%d", result.PRLifecycle.Merged)
|
|
closedPRs = fmt.Sprintf("%d", result.PRLifecycle.ClosedOrRejected)
|
|
}
|
|
reviewedPRs, unreviewedPRs, needsReReviewPRs := "N/A", "N/A", "N/A"
|
|
if result.PRReviewAudit != nil {
|
|
reviewedPRs = fmt.Sprintf("%d", result.PRReviewAudit.Reviewed)
|
|
unreviewedPRs = fmt.Sprintf("%d", result.PRReviewAudit.Unreviewed)
|
|
needsReReviewPRs = fmt.Sprintf("%d", result.PRReviewAudit.NeedsReReview)
|
|
}
|
|
if _, err := fmt.Fprintln(tw, "REPOSITORY\tREPORT_SCORE\tRISK\tHEALTH_SCORE\tISSUES_ANALYZED\tHIGH_RISK_ISSUES\tPRS_ANALYZED\tHIGH_RISK_PRS\tOPEN_PRS\tMERGED_PRS\tCLOSED_PRS\tREVIEWED_PRS\tUNREVIEWED_PRS\tNEEDS_RE_REVIEW\tTOP_RECOMMENDATION"); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintf(tw, "%s\t%d\t%s\t%s\t%d\t%d\t%d\t%d\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
|
result.Repository,
|
|
result.ReportScore,
|
|
result.RiskLevel,
|
|
healthScore,
|
|
result.IssueSummary.Total,
|
|
result.IssueSummary.HighRisk,
|
|
result.PRSummary.Total,
|
|
result.PRSummary.HighRisk,
|
|
openPRs,
|
|
mergedPRs,
|
|
closedPRs,
|
|
reviewedPRs,
|
|
unreviewedPRs,
|
|
needsReReviewPRs,
|
|
topRecommendation,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
return tw.Flush()
|
|
}
|
|
|
|
func writeStaleTable(w io.Writer, report StaleReport) error {
|
|
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
|
|
if _, err := fmt.Fprintln(tw, "TYPE\tNUMBER\tBUCKET\tAGE_DAYS\tLAST_ACTIVITY\tCOMMENTS\tACTION\tTITLE"); err != nil {
|
|
return err
|
|
}
|
|
for _, item := range report.Items {
|
|
if _, err := fmt.Fprintf(tw, "%s\t#%d\t%s\t%d\t%s\t%d\t%s\t%s\n",
|
|
item.Kind,
|
|
item.Number,
|
|
item.Bucket,
|
|
item.AgeDays,
|
|
formatStaleActivity(item.LastActivityAt),
|
|
item.CommentsCount,
|
|
truncateTableText(item.SuggestedAction, 52),
|
|
truncateTableText(item.Title, 64),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tw.Flush()
|
|
}
|
|
|
|
func writeTriageMarkdown(w io.Writer, report TriageReport) error {
|
|
if _, err := fmt.Fprintf(w, "# Issue Triage Report\n\nRepository: `%s`\n\n", report.Repository); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(w, "| Issue | Type | Priority | Confidence | Action | Missing Information |"); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(w, "| --- | --- | --- | ---: | --- | --- |"); err != nil {
|
|
return err
|
|
}
|
|
for _, result := range report.Results {
|
|
missing := "-"
|
|
if len(result.MissingInformation) > 0 {
|
|
missing = strings.Join(result.MissingInformation, ", ")
|
|
}
|
|
if _, err := fmt.Fprintf(w, "| #%d | %s | %s | %d | %s | %s |\n",
|
|
result.Issue.Number,
|
|
result.DetectedType,
|
|
result.Priority,
|
|
result.Confidence,
|
|
result.RecommendedAction,
|
|
missing,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeRepoReportMarkdown(w io.Writer, result RepoReportResult, lang string) error {
|
|
lang = normalizeLang(lang)
|
|
if _, err := fmt.Fprintf(w, "# %s\n\n", repoReportText(lang, "title")); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintf(w, "## %s\n\n", repoReportText(lang, "overview")); err != nil {
|
|
return err
|
|
}
|
|
healthScore := repoReportText(lang, "not_available")
|
|
if result.Health != nil {
|
|
healthScore = fmt.Sprintf("%d", result.Health.HealthScore)
|
|
}
|
|
overview := []string{
|
|
fmt.Sprintf("- Repository: `%s`", result.Repository),
|
|
fmt.Sprintf("- Report score: `%d`", result.ReportScore),
|
|
fmt.Sprintf("- Risk level: `%s`", result.RiskLevel),
|
|
fmt.Sprintf("- Health score: `%s`", healthScore),
|
|
fmt.Sprintf("- Issues analyzed: `%d`", result.IssueSummary.Total),
|
|
fmt.Sprintf("- Pull requests analyzed: `%d`", result.PRSummary.Total),
|
|
fmt.Sprintf("- Source: `%s`", result.Source),
|
|
}
|
|
for _, line := range overview {
|
|
if _, err := fmt.Fprintln(w, line); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err := fmt.Fprintf(w, "\n## %s\n\n", repoReportText(lang, "health")); err != nil {
|
|
return err
|
|
}
|
|
if result.Health == nil {
|
|
if _, err := fmt.Fprintf(w, "- %s\n", repoReportText(lang, "not_available")); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if _, err := fmt.Fprintf(w, "- Score: `%d`\n- Risk: `%s`\n", result.Health.HealthScore, result.Health.RiskLevel); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err := fmt.Fprintf(w, "\n## %s\n\n", repoReportText(lang, "issues")); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintf(w, "- Total: `%d`\n- High risk: `%d`\n- Missing information: `%d`\n", result.IssueSummary.Total, result.IssueSummary.HighRisk, result.IssueSummary.MissingInfo); err != nil {
|
|
return err
|
|
}
|
|
writeCountMapMarkdown(w, "By type", result.IssueSummary.ByType)
|
|
writeCountMapMarkdown(w, "By priority", result.IssueSummary.ByPriority)
|
|
|
|
if _, err := fmt.Fprintf(w, "\n## %s\n\n", repoReportText(lang, "prs")); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintf(w, "- Total: `%d`\n- High risk: `%d`\n", result.PRSummary.Total, result.PRSummary.HighRisk); err != nil {
|
|
return err
|
|
}
|
|
writeCountMapMarkdown(w, "By type", result.PRSummary.ByType)
|
|
writeCountMapMarkdown(w, "By risk", result.PRSummary.ByRisk)
|
|
writeCountMapMarkdown(w, "Risk rule sources", result.PRSummary.RiskSources)
|
|
if result.PRLifecycle != nil {
|
|
if _, err := fmt.Fprintf(w, "- Lifecycle totals: open `%d`, merged `%d`, closed/rejected `%d`, all states `%d`\n",
|
|
result.PRLifecycle.Open,
|
|
result.PRLifecycle.Merged,
|
|
result.PRLifecycle.ClosedOrRejected,
|
|
result.PRLifecycle.Total,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if result.PRReviewAudit != nil {
|
|
if _, err := fmt.Fprintf(w, "- Review audit: audited `%d`, reviewed `%d`, unreviewed `%d`, needs re-review `%d`, formal reviews `%d`\n",
|
|
result.PRReviewAudit.Audited,
|
|
result.PRReviewAudit.Reviewed,
|
|
result.PRReviewAudit.Unreviewed,
|
|
result.PRReviewAudit.NeedsReReview,
|
|
result.PRReviewAudit.FormalReviews,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintf(w, "- Review actor attribution: reviewer comments `%d`, submitter comments `%d`, participant comments `%d`, system events `%d`\n",
|
|
result.PRReviewAudit.ReviewerComments,
|
|
result.PRReviewAudit.SubmitterComments,
|
|
result.PRReviewAudit.ParticipantComments,
|
|
result.PRReviewAudit.SystemEvents,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(result.PRSummary.ReviewFocus) > 0 {
|
|
if _, err := fmt.Fprintln(w, "- Review focus:"); err != nil {
|
|
return err
|
|
}
|
|
for _, focus := range result.PRSummary.ReviewFocus {
|
|
if _, err := fmt.Fprintf(w, " - %s\n", focus); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := writeRepoReportMarkdownList(w, repoReportText(lang, "recommendations"), result.Recommendations, repoReportText(lang, "not_available")); err != nil {
|
|
return err
|
|
}
|
|
return writeRepoReportMarkdownList(w, repoReportText(lang, "reasoning"), result.Reasoning, repoReportText(lang, "not_available"))
|
|
}
|
|
|
|
func writeStaleMarkdown(w io.Writer, report StaleReport, lang string) error {
|
|
lang = normalizeLang(lang)
|
|
if _, err := fmt.Fprintf(w, "# %s\n\n", staleText(lang, "title")); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintf(w, "## %s\n\n", staleText(lang, "overview")); err != nil {
|
|
return err
|
|
}
|
|
lines := []string{
|
|
fmt.Sprintf("- Repository: `%s`", report.Repository),
|
|
fmt.Sprintf("- Source: `%s`", report.Source),
|
|
fmt.Sprintf("- State: `%s`", report.State),
|
|
fmt.Sprintf("- Stale threshold: `%d` days", report.StaleDays),
|
|
fmt.Sprintf("- Scanned: `%d`", report.ScannedTotal),
|
|
fmt.Sprintf("- Flagged: `%d`", report.FlaggedTotal),
|
|
fmt.Sprintf("- Shown: `%d`", report.ShownTotal),
|
|
fmt.Sprintf("- Omitted: `%d`", report.OmittedTotal),
|
|
fmt.Sprintf("- Oldest age: `%d` days", report.OldestAgeDays),
|
|
fmt.Sprintf("- Buckets: `fresh=%d`, `watch=%d`, `stale=%d`, `zombie=%d`", report.ByBucket[staleBucketFresh], report.ByBucket[staleBucketWatch], report.ByBucket[staleBucketStale], report.ByBucket[staleBucketZombie]),
|
|
}
|
|
for _, line := range lines {
|
|
if _, err := fmt.Fprintln(w, line); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err := fmt.Fprintf(w, "\n## %s\n\n", staleText(lang, "items")); err != nil {
|
|
return err
|
|
}
|
|
if len(report.Items) == 0 {
|
|
if _, err := fmt.Fprintf(w, "- %s\n", staleText(lang, "no_items")); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if _, err := fmt.Fprintln(w, "| Type | Number | Bucket | Age (days) | Last activity | Action | Title |"); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(w, "| --- | ---: | --- | ---: | --- | --- | --- |"); err != nil {
|
|
return err
|
|
}
|
|
for _, item := range report.Items {
|
|
if _, err := fmt.Fprintf(w, "| %s | #%d | %s | %d | %s | %s | %s |\n",
|
|
item.Kind,
|
|
item.Number,
|
|
item.Bucket,
|
|
item.AgeDays,
|
|
formatStaleActivity(item.LastActivityAt),
|
|
item.SuggestedAction,
|
|
strings.ReplaceAll(item.Title, "|", "\\|"),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if _, err := fmt.Fprintf(w, "\n## %s\n\n", staleText(lang, "recommendations")); err != nil {
|
|
return err
|
|
}
|
|
for _, recommendation := range report.Recommendations {
|
|
if _, err := fmt.Fprintf(w, "- %s\n", recommendation); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err := fmt.Fprintf(w, "\n## %s\n\n", staleText(lang, "notes")); err != nil {
|
|
return err
|
|
}
|
|
if len(report.Notes) == 0 {
|
|
_, err := fmt.Fprintf(w, "- %s\n", staleText(lang, "no_notes"))
|
|
return err
|
|
}
|
|
for _, note := range report.Notes {
|
|
if _, err := fmt.Fprintf(w, "- %s\n", note); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeCountMapMarkdown(w io.Writer, title string, values map[string]int) error {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
keys := make([]string, 0, len(values))
|
|
for key := range values {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
if _, err := fmt.Fprintf(w, "- %s:\n", title); err != nil {
|
|
return err
|
|
}
|
|
for _, key := range keys {
|
|
if _, err := fmt.Fprintf(w, " - `%s`: `%d`\n", key, values[key]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeRepoReportMarkdownList(w io.Writer, title string, values []string, fallback string) error {
|
|
if _, err := fmt.Fprintf(w, "\n## %s\n\n", title); err != nil {
|
|
return err
|
|
}
|
|
if len(values) == 0 {
|
|
_, err := fmt.Fprintf(w, "- %s\n", fallback)
|
|
return err
|
|
}
|
|
for _, value := range values {
|
|
if _, err := fmt.Fprintf(w, "- %s\n", value); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writePRSummaryMarkdown(w io.Writer, result PRSummaryResult, lang string) error {
|
|
lang = normalizeLang(lang)
|
|
if _, err := fmt.Fprintf(w, "# %s\n\n", message(lang, "pr_summary_title")); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintf(w, "## %s\n\n", message(lang, "pr_summary_overview")); err != nil {
|
|
return err
|
|
}
|
|
lines := []string{
|
|
fmt.Sprintf("- Repository: `%s`", result.Repository),
|
|
fmt.Sprintf("- PR: `#%d` %s", result.Number, result.Title),
|
|
fmt.Sprintf("- Author: `%s`", result.Author),
|
|
fmt.Sprintf("- State: `%s`", result.State),
|
|
fmt.Sprintf("- Base branch: `%s`", result.BaseBranch),
|
|
fmt.Sprintf("- Head branch: `%s`", result.HeadBranch),
|
|
fmt.Sprintf("- Change type: `%s`", result.ChangeType),
|
|
fmt.Sprintf("- Risk level: `%s`", result.RiskLevel),
|
|
fmt.Sprintf("- Changed files: `%d`", result.ChangedFilesCount),
|
|
fmt.Sprintf("- Commits: `%d`", result.CommitCount),
|
|
fmt.Sprintf("- Source: `%s`", result.Source),
|
|
}
|
|
for _, line := range lines {
|
|
if _, err := fmt.Fprintln(w, line); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := writePRSummaryMarkdownList(w, message(lang, "pr_summary_review_focus"), result.ReviewFocus, message(lang, "pr_summary_no_focus")); err != nil {
|
|
return err
|
|
}
|
|
if err := writePRSummaryMarkdownList(w, message(lang, "pr_summary_test_suggestions"), result.TestSuggestions, message(lang, "pr_summary_no_suggestions")); err != nil {
|
|
return err
|
|
}
|
|
if err := writePRSummaryMarkdownList(w, message(lang, "pr_summary_merge_checklist"), result.MergeChecklist, message(lang, "pr_summary_no_checklist")); err != nil {
|
|
return err
|
|
}
|
|
return writePRSummaryMarkdownList(w, message(lang, "pr_summary_reasoning"), result.Reasoning, message(lang, "pr_summary_no_reasoning"))
|
|
}
|
|
|
|
func writePRSummaryMarkdownList(w io.Writer, title string, values []string, fallback string) error {
|
|
if _, err := fmt.Fprintf(w, "\n## %s\n\n", title); err != nil {
|
|
return err
|
|
}
|
|
if len(values) == 0 {
|
|
_, err := fmt.Fprintf(w, "- %s\n", fallback)
|
|
return err
|
|
}
|
|
for _, value := range values {
|
|
if _, err := fmt.Fprintf(w, "- %s\n", value); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeHealthMarkdown(w io.Writer, result HealthResult) error {
|
|
if _, err := fmt.Fprintf(w, "# Repository Health Report\n\nRepository: `%s`\n\nHealth score: **%d**\n\nRisk level: **%s**\n\n", result.Repository, result.HealthScore, result.RiskLevel); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(w, "| Metric | Status | Score | Max | Reason |"); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(w, "| --- | --- | ---: | ---: | --- |"); err != nil {
|
|
return err
|
|
}
|
|
for _, metric := range result.Metrics {
|
|
if _, err := fmt.Fprintf(w, "| %s | %s | %d | %d | %s |\n", metric.Name, metric.Status, metric.Score, metric.MaxScore, metric.Reason); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(result.Recommendations) == 0 {
|
|
return nil
|
|
}
|
|
if _, err := fmt.Fprintln(w, "\n## Recommendations"); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintln(w); err != nil {
|
|
return err
|
|
}
|
|
for _, recommendation := range result.Recommendations {
|
|
if _, err := fmt.Fprintf(w, "- %s\n", recommendation); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func truncateTableText(value string, max int) string {
|
|
value = strings.Join(strings.Fields(value), " ")
|
|
runes := []rune(value)
|
|
if max <= 0 || len(runes) <= max {
|
|
return value
|
|
}
|
|
if max <= 3 {
|
|
return string(runes[:max])
|
|
}
|
|
return string(runes[:max-3]) + "..."
|
|
}
|
|
|
|
func formatStaleActivity(value time.Time) string {
|
|
if value.IsZero() {
|
|
return "-"
|
|
}
|
|
return value.Format("2006-01-02")
|
|
}
|