package main import ( "context" "fmt" "io" "log/slog" "os" "strings" "github.com/nomadcode/nomadcode-core/internal/adapters/plane" ) type EnvConfig struct { BaseURL string Token string WorkspaceSlug string ProjectID string WorkItemID string SmokeApply bool SmokeComment string SmokeStateID string } func LoadConfig(getenv func(string) string) (EnvConfig, error) { var cfg EnvConfig cfg.BaseURL = strings.TrimSpace(getenv("PLANE_BASE_URL")) cfg.Token = strings.TrimSpace(getenv("PLANE_TOKEN")) cfg.WorkspaceSlug = strings.TrimSpace(getenv("PLANE_WORKSPACE_SLUG")) cfg.ProjectID = strings.TrimSpace(getenv("PLANE_PROJECT_ID")) cfg.WorkItemID = strings.TrimSpace(getenv("PLANE_WORK_ITEM_ID")) var missing []string if cfg.BaseURL == "" { missing = append(missing, "PLANE_BASE_URL") } if cfg.Token == "" { missing = append(missing, "PLANE_TOKEN") } if cfg.WorkspaceSlug == "" { missing = append(missing, "PLANE_WORKSPACE_SLUG") } if cfg.ProjectID == "" { missing = append(missing, "PLANE_PROJECT_ID") } if cfg.WorkItemID == "" { missing = append(missing, "PLANE_WORK_ITEM_ID") } if len(missing) > 0 { return cfg, fmt.Errorf("missing required environment variables: %s", strings.Join(missing, ", ")) } applyStr := getenv("PLANE_SMOKE_APPLY") cfg.SmokeApply = applyStr == "1" || strings.ToLower(applyStr) == "true" if cfg.SmokeApply { cfg.SmokeComment = strings.TrimSpace(getenv("PLANE_SMOKE_COMMENT")) cfg.SmokeStateID = strings.TrimSpace(getenv("PLANE_SMOKE_STATE_ID")) var missingSmoke []string if cfg.SmokeComment == "" { missingSmoke = append(missingSmoke, "PLANE_SMOKE_COMMENT") } if cfg.SmokeStateID == "" { missingSmoke = append(missingSmoke, "PLANE_SMOKE_STATE_ID") } if len(missingSmoke) > 0 { return cfg, fmt.Errorf("PLANE_SMOKE_APPLY is enabled but missing required smoke variables: %s", strings.Join(missingSmoke, ", ")) } } return cfg, nil } type PlaneClient interface { GetWorkItem(ctx context.Context, ref plane.WorkItemRef) (plane.WorkItem, error) AddComment(ctx context.Context, input plane.AddCommentInput) error UpdateIssueStatus(ctx context.Context, input plane.UpdateIssueStatusInput) error } func RunSmoke(ctx context.Context, cfg EnvConfig, client PlaneClient, out io.Writer) error { ref := plane.WorkItemRef{ WorkspaceSlug: cfg.WorkspaceSlug, ProjectID: cfg.ProjectID, WorkItemID: cfg.WorkItemID, } item, err := client.GetWorkItem(ctx, ref) if err != nil { return fmt.Errorf("failed to lookup work item: %w", err) } fmt.Fprintf(out, "lookup ok: id=%s name=%s\n", item.ID, item.Name) if !cfg.SmokeApply { fmt.Fprintln(out, "write smoke skipped: set PLANE_SMOKE_APPLY=1") return nil } err = client.AddComment(ctx, plane.AddCommentInput{ Ref: ref, CommentHTML: fmt.Sprintf("

%s

", cfg.SmokeComment), ExternalSource: "nomadcode", ExternalID: "plane-smoke-test", }) if err != nil { return fmt.Errorf("failed to add comment: %w", err) } fmt.Fprintln(out, "comment ok") err = client.UpdateIssueStatus(ctx, plane.UpdateIssueStatusInput{ Ref: ref, Status: cfg.SmokeStateID, }) if err != nil { return fmt.Errorf("failed to update state: %w", err) } fmt.Fprintf(out, "state update ok: state_id=%s\n", cfg.SmokeStateID) return nil } func main() { cfg, err := LoadConfig(os.Getenv) if err != nil { fmt.Fprintf(os.Stderr, "Configuration error: %v\n", err) fmt.Fprintln(os.Stderr, "\nUsage:") fmt.Fprintln(os.Stderr, " Required environment variables:") fmt.Fprintln(os.Stderr, " PLANE_BASE_URL - Base URL of Plane instance") fmt.Fprintln(os.Stderr, " PLANE_TOKEN - API Token (will be redacted in logs)") fmt.Fprintln(os.Stderr, " PLANE_WORKSPACE_SLUG - Workspace slug (e.g. general)") fmt.Fprintln(os.Stderr, " PLANE_PROJECT_ID - Project UUID") fmt.Fprintln(os.Stderr, " PLANE_WORK_ITEM_ID - Work Item UUID") fmt.Fprintln(os.Stderr, " Optional variables for write operations:") fmt.Fprintln(os.Stderr, " PLANE_SMOKE_APPLY - Set to 1 to enable write smoke tests") fmt.Fprintln(os.Stderr, " PLANE_SMOKE_COMMENT - HTML comment content to post") fmt.Fprintln(os.Stderr, " PLANE_SMOKE_STATE_ID - State UUID to transition to") os.Exit(1) } safeURL := cfg.BaseURL safeToken := "" logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) logger.Info("starting plane smoke test", "base_url", safeURL, "token", safeToken, "workspace", cfg.WorkspaceSlug, "project_id", cfg.ProjectID, "work_item_id", cfg.WorkItemID, "apply", cfg.SmokeApply, ) planeClient := plane.NewClient(plane.Config{ BaseURL: cfg.BaseURL, Token: cfg.Token, }, logger) if err := RunSmoke(context.Background(), cfg, planeClient, os.Stdout); err != nil { logger.Error("smoke test failed", "error", redactToken(err.Error(), cfg.Token)) os.Exit(2) } logger.Info("plane smoke test finished successfully") } func redactToken(s, token string) string { if token == "" { return s } return strings.ReplaceAll(s, token, "") }