Files
poimen-workflows/action/notification.go
T

299 lines
8.7 KiB
Go
Raw Normal View History

package action
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"time"
)
// NotifyStatusInput is input for NotifyStatusActivity
type NotifyStatusInput struct {
Channel string `json:"channel"` // slack, email, webhook
Status string `json:"status"` // success, failure, warning
Message string `json:"message"`
}
// NotifyStatusOutput is output from NotifyStatusActivity
type NotifyStatusOutput struct {
NotificationID string `json:"notificationId"`
Timestamp string `json:"timestamp"`
}
// NotifyStatusActivity sends notifications
func NotifyStatusActivity(ctx context.Context, in NotifyStatusInput) (NotifyStatusOutput, error) {
logger := newActivityLogger(ctx)
logger.Info("NotifyStatusActivity started", "channel", in.Channel, "status", in.Status)
timestamp := time.Now().Format(time.RFC3339)
notificationID := fmt.Sprintf("notify-%d", time.Now().UnixNano())
switch in.Channel {
case "slack":
if err := sendSlackNotification(ctx, in); err != nil {
logger.Warn("Slack notification failed", "error", err)
// Don't fail the activity, just log
}
case "webhook":
if err := sendWebhookNotification(ctx, in); err != nil {
logger.Warn("Webhook notification failed", "error", err)
}
case "email":
// Email would require SMTP setup - log for now
logger.Info("Email notification (logged)", "message", in.Message)
default:
logger.Info("Notification logged", "channel", in.Channel, "message", in.Message)
}
logger.Info("NotifyStatusActivity completed", "notificationId", notificationID)
return NotifyStatusOutput{
NotificationID: notificationID,
Timestamp: timestamp,
}, nil
}
func sendSlackNotification(ctx context.Context, in NotifyStatusInput) error {
webhookURL := os.Getenv("SLACK_WEBHOOK_URL")
if webhookURL == "" {
return fmt.Errorf("SLACK_WEBHOOK_URL not set")
}
// Map status to emoji
emoji := "️"
switch in.Status {
case "success":
emoji = "✅"
case "failure":
emoji = "❌"
case "warning":
emoji = "⚠️"
}
payload := map[string]string{
"text": fmt.Sprintf("%s *%s*: %s", emoji, in.Status, in.Message),
}
body, _ := json.Marshal(payload)
req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("slack returned status %d", resp.StatusCode)
}
return nil
}
func sendWebhookNotification(ctx context.Context, in NotifyStatusInput) error {
webhookURL := os.Getenv("NOTIFICATION_WEBHOOK_URL")
if webhookURL == "" {
return fmt.Errorf("NOTIFICATION_WEBHOOK_URL not set")
}
payload := map[string]interface{}{
"channel": in.Channel,
"status": in.Status,
"message": in.Message,
"timestamp": time.Now().Format(time.RFC3339),
}
body, _ := json.Marshal(payload)
req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("webhook returned status %d", resp.StatusCode)
}
return nil
}
// ArchiveResultsInput is input for ArchiveResultsActivity
type ArchiveResultsInput struct {
ReportPath string `json:"reportPath"`
Destination string `json:"destination"` // s3://bucket/path or local path
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// ArchiveResultsOutput is output from ArchiveResultsActivity
type ArchiveResultsOutput struct {
ArchiveURL string `json:"archiveUrl"`
ArchiveSize int64 `json:"archiveSize"`
}
// ArchiveResultsActivity archives results to storage
func ArchiveResultsActivity(ctx context.Context, in ArchiveResultsInput) (ArchiveResultsOutput, error) {
logger := newActivityLogger(ctx)
logger.Info("ArchiveResultsActivity started", "reportPath", in.ReportPath, "destination", in.Destination)
// Check if source exists
info, err := os.Stat(in.ReportPath)
if err != nil {
return ArchiveResultsOutput{}, fmt.Errorf("report not found: %s", in.ReportPath)
}
// For now, just copy to local destination or log for cloud
var archiveURL string
var archiveSize int64
if len(in.Destination) > 5 && in.Destination[:5] == "s3://" {
// Would use AWS SDK - for now just log
logger.Info("Would upload to S3", "destination", in.Destination)
archiveURL = in.Destination
archiveSize = info.Size()
} else {
// Copy to local destination
content, err := os.ReadFile(in.ReportPath)
if err != nil {
return ArchiveResultsOutput{}, fmt.Errorf("failed to read report: %w", err)
}
destPath := in.Destination
if destPath == "" {
destPath = fmt.Sprintf("/tmp/archive-%d", time.Now().UnixNano())
}
if err := os.WriteFile(destPath, content, 0644); err != nil {
return ArchiveResultsOutput{}, fmt.Errorf("failed to write archive: %w", err)
}
archiveURL = destPath
archiveSize = int64(len(content))
}
logger.Info("ArchiveResultsActivity completed", "archiveUrl", archiveURL, "size", archiveSize)
return ArchiveResultsOutput{
ArchiveURL: archiveURL,
ArchiveSize: archiveSize,
}, nil
}
// DeploymentPreCheckInput is input for DeploymentPreCheckActivity
type DeploymentPreCheckInput struct {
Path string `json:"path"`
CheckType string `json:"checkType,omitempty"` // lint, test, build, all
}
// DeploymentPreCheckOutput is output from DeploymentPreCheckActivity
type DeploymentPreCheckOutput struct {
Passed bool `json:"passed"`
Failures []string `json:"failures"`
Warnings []string `json:"warnings"`
}
// DeploymentPreCheckActivity validates deployment readiness
func DeploymentPreCheckActivity(ctx context.Context, in DeploymentPreCheckInput) (DeploymentPreCheckOutput, error) {
logger := newActivityLogger(ctx)
logger.Info("DeploymentPreCheckActivity started", "path", in.Path, "checkType", in.CheckType)
output := DeploymentPreCheckOutput{
Passed: true,
Failures: []string{},
Warnings: []string{},
}
checkType := in.CheckType
if checkType == "" {
checkType = "all"
}
// Check if path exists
if _, err := os.Stat(in.Path); os.IsNotExist(err) {
output.Passed = false
output.Failures = append(output.Failures, fmt.Sprintf("path does not exist: %s", in.Path))
return output, nil
}
// Check for Go project
isGo := false
if _, err := os.Stat(fmt.Sprintf("%s/go.mod", in.Path)); err == nil {
isGo = true
}
if isGo && (checkType == "all" || checkType == "build") {
// Try go build
cmd := exec.CommandContext(ctx, "go", "build", "./...")
cmd.Dir = in.Path
if buildOut, err := cmd.CombinedOutput(); err != nil {
output.Passed = false
output.Failures = append(output.Failures, fmt.Sprintf("build failed: %s", string(buildOut)))
}
}
if isGo && (checkType == "all" || checkType == "test") {
// Try go test
cmd := exec.CommandContext(ctx, "go", "test", "-short", "./...")
cmd.Dir = in.Path
if testOut, err := cmd.CombinedOutput(); err != nil {
output.Passed = false
output.Failures = append(output.Failures, fmt.Sprintf("tests failed: %s", string(testOut)))
}
}
if isGo && (checkType == "all" || checkType == "lint") {
// Try go vet
cmd := exec.CommandContext(ctx, "go", "vet", "./...")
cmd.Dir = in.Path
if vetOut, err := cmd.CombinedOutput(); err != nil {
output.Warnings = append(output.Warnings, fmt.Sprintf("vet issues: %s", string(vetOut)))
}
}
logger.Info("DeploymentPreCheckActivity completed", "passed", output.Passed, "failures", len(output.Failures))
return output, nil
}
// ApproveWorkflowInput is input for ApproveWorkflowActivity
type ApproveWorkflowInput struct {
WorkflowID string `json:"workflowId"`
RequiredApprovals int `json:"requiredApprovals,omitempty"`
TimeoutMinutes int `json:"timeoutMinutes,omitempty"`
}
// ApproveWorkflowOutput is output from ApproveWorkflowActivity
type ApproveWorkflowOutput struct {
Approved bool `json:"approved"`
Approver string `json:"approver,omitempty"`
Timestamp string `json:"timestamp"`
}
// ApproveWorkflowActivity handles approval workflow (auto-approves for now)
func ApproveWorkflowActivity(ctx context.Context, in ApproveWorkflowInput) (ApproveWorkflowOutput, error) {
logger := newActivityLogger(ctx)
logger.Info("ApproveWorkflowActivity started", "workflowId", in.WorkflowID)
// For now, auto-approve
// In production, this would wait for human approval via signal or external system
timestamp := time.Now().Format(time.RFC3339)
logger.Info("ApproveWorkflowActivity completed (auto-approved)")
return ApproveWorkflowOutput{
Approved: true,
Approver: "system-auto",
Timestamp: timestamp,
}, nil
}