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"` } // checker defines a deployment check type checker struct { name string types []string // which checkTypes trigger this checker isWarning bool // if true, failure goes to warnings not failures run func(ctx context.Context, path string) (string, error) } // shouldRun checks if this checker should run for given checkType func (c *checker) shouldRun(checkType string) bool { for _, t := range c.types { if t == checkType { return true } } return false } // goCheckers returns checkers for Go projects func goCheckers() []*checker { return []*checker{ { name: "build", types: []string{"all", "build"}, run: func(ctx context.Context, path string) (string, error) { cmd := exec.CommandContext(ctx, "go", "build", "./...") cmd.Dir = path out, err := cmd.CombinedOutput() return string(out), err }, }, { name: "test", types: []string{"all", "test"}, run: func(ctx context.Context, path string) (string, error) { cmd := exec.CommandContext(ctx, "go", "test", "-short", "./...") cmd.Dir = path out, err := cmd.CombinedOutput() return string(out), err }, }, { name: "lint", types: []string{"all", "lint"}, isWarning: true, run: func(ctx context.Context, path string) (string, error) { cmd := exec.CommandContext(ctx, "go", "vet", "./...") cmd.Dir = path out, err := cmd.CombinedOutput() return string(out), err }, }, } } // detectProjectCheckers returns checkers based on project type func detectProjectCheckers(path string) []*checker { if _, err := os.Stat(fmt.Sprintf("%s/go.mod", path)); err == nil { return goCheckers() } // Add more project types here (Node, Python, etc.) return nil } // 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 } // Run applicable checkers for _, c := range detectProjectCheckers(in.Path) { if !c.shouldRun(checkType) { continue } if out, err := c.run(ctx, in.Path); err != nil { msg := fmt.Sprintf("%s failed: %s", c.name, out) if c.isWarning { output.Warnings = append(output.Warnings, msg) } else { output.Passed = false output.Failures = append(output.Failures, msg) } } } 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 }