package main import ( "flag" "fmt" "os/exec" "strings" ) // runSeedJiraData provisions (or updates) the Jira module that pushes kanban // changes to soporte-anjana.atlassian.net, project DATA, board 33. // // Credentials are read from `pass` so they never appear in argv or env. The // API token, email, and domain are loaded from the canonical entries: // // pass jira/anjana/api-token // pass jira/anjana/email // pass jira/anjana/domain // // Defaults can be overridden with flags (project, board, name, filter). // // Idempotent: if a module with the same name already exists, its config is // rewritten (encrypted at rest by saveModule). The kanban module key // (KANBAN_MODULE_KEY env var) must be set — the same value the running server // uses, otherwise the server cannot decrypt the secrets we wrote. func runSeedJiraData(args []string) error { fs := flag.NewFlagSet("kanban seed-jira-data", flag.ContinueOnError) dbPath := fs.String("db", "operations.db", "SQLite database path") name := fs.String("name", "Jira DATA", "Module display name (also used as upsert key)") project := fs.String("project", "DATA", "Jira project key (e.g. DATA)") board := fs.Int("board", 33, "Jira board id (Agile board; informational + validated at /test)") filter := fs.String("event-filter", "card.created,card.updated,card.moved,message.created", "Comma-separated event types this module subscribes to") enabled := fs.Bool("enabled", true, "Start with module enabled (true) or disabled (false)") passEntry := fs.String("pass-prefix", "jira/anjana", "pass entry prefix; reads ${prefix}/{email,api-token,domain}") if err := fs.Parse(args); err != nil { return err } email, err := passShow(*passEntry + "/email") if err != nil { return fmt.Errorf("read email from pass: %w", err) } token, err := passShow(*passEntry + "/api-token") if err != nil { return fmt.Errorf("read api-token from pass: %w", err) } domain, err := passShow(*passEntry + "/domain") if err != nil { return fmt.Errorf("read domain from pass: %w", err) } baseURL := "https://" + strings.TrimSpace(domain) db, err := openDB(*dbPath) if err != nil { return fmt.Errorf("open db: %w", err) } defer db.Close() cfg := JSONValue{ "base_url": baseURL, "email": email, "api_token": token, "project_key": *project, "board_id": *board, "status_map": map[string]string{}, // operator fills via UI (column name → Jira status) } // Upsert by name. Module name is the human-friendly identifier; we treat // it as unique for the purposes of seeding so re-running this command does // not duplicate the row. mods, err := db.listModulesAll() if err != nil { return fmt.Errorf("list modules: %w", err) } var existing *Module for i := range mods { if mods[i].Name == *name { existing = &mods[i] break } } if existing != nil { existing.Kind = "jira" existing.Enabled = *enabled existing.EventFilter = splitCSV(*filter) existing.Config = cfg if err := db.saveModule(existing); err != nil { return fmt.Errorf("update module: %w", err) } fmt.Printf("updated module %q (id=%s)\n", existing.Name, existing.ID) return nil } m := &Module{ Name: *name, Kind: "jira", Enabled: *enabled, EventFilter: splitCSV(*filter), Config: cfg, } if err := db.saveModule(m); err != nil { return fmt.Errorf("create module: %w", err) } fmt.Printf("created module %q (id=%s)\n", m.Name, m.ID) fmt.Printf("project: %s board: %d base_url: %s email: %s\n", *project, *board, baseURL, email) fmt.Println("\nnext steps:") fmt.Println(" 1. Edit status_map in the Modulos UI: map kanban column names to Jira statuses") fmt.Println(" (e.g. \"In Progress\" → \"In Progress\", \"Done\" → \"Done\")") fmt.Println(" 2. Click \"Test\" in the UI to verify board 33 belongs to project DATA") fmt.Println(" 3. Move a card in kanban — push should hit Jira REST API") return nil } // passShow shells out to pass(1) to read a secret. We do not cache or print // the value; just trim trailing whitespace before returning. func passShow(entry string) (string, error) { out, err := exec.Command("pass", "show", entry).Output() if err != nil { return "", fmt.Errorf("pass show %s: %w", entry, err) } return strings.TrimSpace(string(out)), nil }