From 34a33a29dc1eb940833b2d4dca5f7663e86af157 Mon Sep 17 00:00:00 2001 From: Phil Austin Date: Sun, 30 Nov 2025 08:17:49 -0800 Subject: [PATCH 1/2] Adding GitHub actions annotations on success --- internal/cmd/track_build.go | 4 +++ internal/cmd/track_deployment.go | 3 ++ internal/github/annotations.go | 59 ++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/internal/cmd/track_build.go b/internal/cmd/track_build.go index 2217f94..dfa3af5 100644 --- a/internal/cmd/track_build.go +++ b/internal/cmd/track_build.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/viper" "github.com/versioner-io/versioner-cli/internal/api" "github.com/versioner-io/versioner-cli/internal/cicd" + "github.com/versioner-io/versioner-cli/internal/github" "github.com/versioner-io/versioner-cli/internal/status" ) @@ -223,5 +224,8 @@ func runBuildTrack(cmd *cobra.Command, args []string) error { fmt.Printf(" Version ID: %s\n", resp.VersionID) } + // Write GitHub Actions job summary + github.WriteSuccessSummary("Build", product, statusValue, version, event.SCMSha, event.BuildURL) + return nil } diff --git a/internal/cmd/track_deployment.go b/internal/cmd/track_deployment.go index 0a95ced..61ed245 100644 --- a/internal/cmd/track_deployment.go +++ b/internal/cmd/track_deployment.go @@ -258,6 +258,9 @@ func runDeploymentTrack(cmd *cobra.Command, args []string) error { fmt.Printf(" Environment ID: %s\n", resp.EnvironmentID) } + // Write GitHub Actions job summary + github.WriteSuccessSummary("Deployment", environment, statusValue, version, event.SCMSha, event.DeployURL) + return nil } diff --git a/internal/github/annotations.go b/internal/github/annotations.go index dda05a2..22df2d2 100644 --- a/internal/github/annotations.go +++ b/internal/github/annotations.go @@ -21,6 +21,65 @@ func WriteErrorAnnotation(statusCode int, errorCode, message, ruleName string, r writeJobSummary(statusCode, errorCode, message, ruleName, retryAfter, details) } +// WriteSuccessSummary writes a GitHub Actions job summary for successful deployment tracking +func WriteSuccessSummary(action, environment, status, version, scmSha, deployURL string) { + // Only write summaries if running in GitHub Actions + if os.Getenv("GITHUB_ACTIONS") != "true" { + return + } + + summaryPath := os.Getenv("GITHUB_STEP_SUMMARY") + if summaryPath == "" { + return + } + + // Build the summary + var summary string + summary += "## πŸš€ Versioner Summary\n\n" + + // Add key information + summary += fmt.Sprintf("- **Action:** %s\n", action) + summary += fmt.Sprintf("- **Environment:** %s\n", environment) + summary += fmt.Sprintf("- **Status:** %s\n", formatStatus(status)) + summary += fmt.Sprintf("- **Version:** `%s`\n", version) + + if scmSha != "" { + summary += fmt.Sprintf("- **Git SHA:** `%s`\n", scmSha) + } + + if deployURL != "" { + summary += fmt.Sprintf("\n[View Deployment Run β†’](%s)\n", deployURL) + } + + // Write to file + f, err := os.OpenFile(summaryPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + // Silently fail - don't break the CLI if we can't write the summary + return + } + defer f.Close() + + _, _ = f.WriteString(summary) +} + +// formatStatus adds an emoji to the status for visual clarity +func formatStatus(status string) string { + switch status { + case "started", "in_progress": + return "⏳ " + status + case "completed", "success": + return "βœ… " + status + case "failed": + return "❌ " + status + case "aborted", "cancelled": + return "🚫 " + status + case "pending": + return "⏸️ " + status + default: + return status + } +} + // writeWorkflowCommand outputs a GitHub Actions workflow command for error annotation func writeWorkflowCommand(statusCode int, errorCode, message, ruleName string) { // Format: ::error title=::<message> From ab0562f51ac65f46905a154e33e1ddee8c998b15 Mon Sep 17 00:00:00 2001 From: Phil Austin <austin.phil@gmail.com> Date: Sun, 30 Nov 2025 08:21:30 -0800 Subject: [PATCH 2/2] Adding GitHub actions annotations on failures --- internal/cmd/track_build.go | 2 ++ internal/cmd/track_deployment.go | 2 ++ internal/github/annotations.go | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/internal/cmd/track_build.go b/internal/cmd/track_build.go index dfa3af5..17d1e1d 100644 --- a/internal/cmd/track_build.go +++ b/internal/cmd/track_build.go @@ -208,10 +208,12 @@ func runBuildTrack(cmd *cobra.Command, args []string) error { if err != nil { if apiErr, ok := err.(*api.APIError); ok { // API error - exit code 2 + github.WriteGenericErrorAnnotation("Build", "API Error", apiErr.Error()) fmt.Fprintf(os.Stderr, "API error: %s\n", apiErr.Error()) os.Exit(2) } // Network or other error - exit code 2 + github.WriteGenericErrorAnnotation("Build", "Network Error", err.Error()) fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) os.Exit(2) } diff --git a/internal/cmd/track_deployment.go b/internal/cmd/track_deployment.go index 61ed245..a5ed2ca 100644 --- a/internal/cmd/track_deployment.go +++ b/internal/cmd/track_deployment.go @@ -241,10 +241,12 @@ func runDeploymentTrack(cmd *cobra.Command, args []string) error { os.Exit(5) // Exit code 5 for preflight failures } // Other API error - exit code 4 + github.WriteGenericErrorAnnotation("Deployment", "API Error", apiErr.Error()) fmt.Fprintf(os.Stderr, "API error: %s\n", apiErr.Error()) os.Exit(4) } // Network or other error - exit code 1 + github.WriteGenericErrorAnnotation("Deployment", "Network Error", err.Error()) fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) os.Exit(1) } diff --git a/internal/github/annotations.go b/internal/github/annotations.go index 22df2d2..4fe9ac1 100644 --- a/internal/github/annotations.go +++ b/internal/github/annotations.go @@ -62,6 +62,63 @@ func WriteSuccessSummary(action, environment, status, version, scmSha, deployURL _, _ = f.WriteString(summary) } +// WriteGenericErrorAnnotation writes a GitHub Actions error annotation for generic failures +// (API errors, network errors, etc.) +func WriteGenericErrorAnnotation(action, errorType, errorMessage string) { + // Only write annotations if running in GitHub Actions + if os.Getenv("GITHUB_ACTIONS") != "true" { + return + } + + summaryPath := os.Getenv("GITHUB_STEP_SUMMARY") + if summaryPath == "" { + return + } + + // Write workflow command annotation + title := fmt.Sprintf("Versioner %s Failed", action) + escapedMessage := escapeWorkflowCommand(errorMessage) + fmt.Fprintf(os.Stdout, "::error title=%s::%s\n", title, escapedMessage) + + // Build the summary + var summary string + summary += fmt.Sprintf("## ❌ Versioner %s Failed\n\n", action) + summary += fmt.Sprintf("### %s\n\n", errorType) + summary += fmt.Sprintf("**Error:** %s\n\n", errorMessage) + + summary += "**Possible Causes:**\n" + switch errorType { + case "API Error": + summary += "- Invalid API key or authentication failure\n" + summary += "- Validation error (check required fields)\n" + summary += "- API service unavailable\n" + summary += "- Rate limiting or quota exceeded\n" + case "Network Error": + summary += "- Network connectivity issues\n" + summary += "- DNS resolution failure\n" + summary += "- API endpoint unreachable\n" + summary += "- Timeout or connection refused\n" + default: + summary += "- Check error message above for details\n" + } + + summary += "\n**Action Required:**\n" + summary += "- Verify your `VERSIONER_API_KEY` is set correctly\n" + summary += "- Check network connectivity to Versioner API\n" + summary += "- Review error message for specific guidance\n" + summary += "- Contact support if issue persists\n" + + // Write to file + f, err := os.OpenFile(summaryPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + // Silently fail - don't break the CLI if we can't write the summary + return + } + defer f.Close() + + _, _ = f.WriteString(summary) +} + // formatStatus adds an emoji to the status for visual clarity func formatStatus(status string) string { switch status {