299 lines
8.7 KiB
Go
299 lines
8.7 KiB
Go
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
|
|||
|
|
}
|