Files
kanban/backend/seed_jira.go
T
egutierrez 5744b82f58 feat(jira): issue_type config + labels_map + status_map default DATA + transition tras create
- jiraConfig: campos IssueType + LabelsMap (kanban col -> labels Jira). Default
  IssueType='Tarea Tecnica' (DATA project no tiene Task).
- create(): usa c.IssueType y aplica labels iniciales. Despues del POST /issue
  ejecuta transitionToStatus para mover la card recien creada al status del
  status_map, asi no aterriza en el initial workflow status (CREADO o To Do)
  sino donde toca segun la columna kanban.
- update() y transition(): aplican labels en cada sync (PUT replaces array).
  Card que sale de Bloqueadas pierde el label 'blocked' automaticamente.
- transitionToStatus: helper compartido entre create() y transition().
- seed-jira-data: inyecta status_map por defecto para nuestras 6 columnas
  (HACIENDO -> In Progress, PNDNT FEEDBACK -> IMPLEMENTADO, HECHO -> Done,
  IDEAS -> CREADO, DEUDA TECNICA -> To Do, Bloqueadas -> In Progress) y
  labels_map (Bloqueadas -> ['blocked']).
- modules_test: mock Jira tambien responde /transitions endpoints.
2026-05-29 11:44:04 +02:00

142 lines
4.7 KiB
Go

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()
// Default mapping for our setup: Kanban columns → Jira `Epicas en Data` board (33)
// statuses. Operator can edit via the Modulos UI once the row exists.
statusMap := map[string]string{
"HACIENDO 🚧": "In Progress",
"PNDNT FEEDBACK ▶️": "IMPLEMENTADO",
"HECHO ✅": "Done",
"IDEAS 💡": "CREADO",
"DEUDA TÉCNICA 🔄": "To Do",
"Bloqueadas": "In Progress",
}
labelsMap := map[string][]string{
"Bloqueadas": {"blocked"},
}
cfg := JSONValue{
"base_url": baseURL,
"email": email,
"api_token": token,
"project_key": *project,
"board_id": *board,
"issue_type": "Tarea Técnica",
"status_map": statusMap,
"labels_map": labelsMap,
}
// 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
}