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.
This commit is contained in:
+77
-30
@@ -480,12 +480,14 @@ func cutoffOK(db *DB, m Module, ev Event) bool {
|
||||
type jiraHandler struct{}
|
||||
|
||||
type jiraConfig struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
Email string `json:"email"`
|
||||
APIToken string `json:"api_token"`
|
||||
ProjectKey string `json:"project_key"`
|
||||
BoardID int `json:"board_id"`
|
||||
StatusMap map[string]string `json:"status_map"`
|
||||
BaseURL string `json:"base_url"`
|
||||
Email string `json:"email"`
|
||||
APIToken string `json:"api_token"`
|
||||
ProjectKey string `json:"project_key"`
|
||||
BoardID int `json:"board_id"`
|
||||
IssueType string `json:"issue_type"` // Jira issuetype name applied on create
|
||||
StatusMap map[string]string `json:"status_map"` // kanban_column_name -> Jira status name
|
||||
LabelsMap map[string][]string `json:"labels_map,omitempty"` // kanban_column_name -> Jira labels (replaces every sync)
|
||||
}
|
||||
|
||||
func parseJiraConfig(m Module) (jiraConfig, error) {
|
||||
@@ -501,6 +503,9 @@ func parseJiraConfig(m Module) (jiraConfig, error) {
|
||||
if c.BaseURL == "" {
|
||||
return c, fmt.Errorf("base_url required")
|
||||
}
|
||||
if c.IssueType == "" {
|
||||
c.IssueType = "Tarea Técnica"
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -615,14 +620,16 @@ func (h *jiraHandler) create(ctx context.Context, db *DB, c jiraConfig, ev Event
|
||||
if c.ProjectKey == "" {
|
||||
return 0, fmt.Errorf("project_key required for create (configure module before pushing)")
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"fields": map[string]interface{}{
|
||||
"project": map[string]string{"key": c.ProjectKey},
|
||||
"summary": card.Title,
|
||||
"description": adfText(card.Description),
|
||||
"issuetype": map[string]string{"name": "Task"},
|
||||
},
|
||||
fields := map[string]interface{}{
|
||||
"project": map[string]string{"key": c.ProjectKey},
|
||||
"summary": card.Title,
|
||||
"description": adfText(card.Description),
|
||||
"issuetype": map[string]string{"name": c.IssueType},
|
||||
}
|
||||
if labels := c.LabelsMap[card.ColumnName]; len(labels) > 0 {
|
||||
fields["labels"] = labels
|
||||
}
|
||||
body := map[string]interface{}{"fields": fields}
|
||||
status, resp, err := h.jiraRequest(ctx, c, http.MethodPost, "/rest/api/3/issue", body)
|
||||
if err != nil {
|
||||
return status, err
|
||||
@@ -631,9 +638,18 @@ func (h *jiraHandler) create(ctx context.Context, db *DB, c jiraConfig, ev Event
|
||||
Key string `json:"key"`
|
||||
}
|
||||
_ = json.Unmarshal(resp, &parsed)
|
||||
if parsed.Key != "" {
|
||||
if err := db.setCardJiraKey(card.ID, parsed.Key); err != nil {
|
||||
return status, fmt.Errorf("link jira key: %w", err)
|
||||
if parsed.Key == "" {
|
||||
return status, fmt.Errorf("jira create returned empty key")
|
||||
}
|
||||
if err := db.setCardJiraKey(card.ID, parsed.Key); err != nil {
|
||||
return status, fmt.Errorf("link jira key: %w", err)
|
||||
}
|
||||
// Jira places new issues in the workflow's initial status (typically
|
||||
// CREADO / To Do for DATA). Drive a transition immediately so the issue
|
||||
// lands in the column that mirrors where the card is in kanban.
|
||||
if _, ok := c.StatusMap[card.ColumnName]; ok {
|
||||
if _, err := h.transitionToStatus(ctx, c, parsed.Key, card.ColumnName); err != nil {
|
||||
return status, fmt.Errorf("created %s but initial transition failed: %w", parsed.Key, err)
|
||||
}
|
||||
}
|
||||
return status, nil
|
||||
@@ -651,20 +667,26 @@ func (h *jiraHandler) update(ctx context.Context, db *DB, c jiraConfig, ev Event
|
||||
// Card not yet linked — bootstrap by creating it.
|
||||
return h.create(ctx, db, c, ev)
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"fields": map[string]interface{}{
|
||||
"summary": card.Title,
|
||||
"description": adfText(card.Description),
|
||||
},
|
||||
fields := map[string]interface{}{
|
||||
"summary": card.Title,
|
||||
"description": adfText(card.Description),
|
||||
}
|
||||
// Labels are derived from the current kanban column. We always send them
|
||||
// (even an empty array) so a card that leaves a labelled column gets its
|
||||
// label removed from Jira — PUT fields.labels REPLACES the whole array.
|
||||
labels := c.LabelsMap[card.ColumnName]
|
||||
if labels == nil {
|
||||
labels = []string{}
|
||||
}
|
||||
fields["labels"] = labels
|
||||
body := map[string]interface{}{"fields": fields}
|
||||
status, _, err := h.jiraRequest(ctx, c, http.MethodPut, "/rest/api/3/issue/"+card.JiraKey, body)
|
||||
return status, err
|
||||
}
|
||||
|
||||
// transition uses the configured status_map to translate the kanban column
|
||||
// to a Jira transition name. We list available transitions, find the one
|
||||
// whose target status name matches, and POST it. Kanban remains the source
|
||||
// of truth even if Jira's current state differs.
|
||||
// to a Jira transition name. Kanban remains the source of truth even if
|
||||
// Jira's current state differs.
|
||||
func (h *jiraHandler) transition(ctx context.Context, db *DB, c jiraConfig, ev Event) (int, error) {
|
||||
if ev.CardID == "" {
|
||||
return 0, nil
|
||||
@@ -676,11 +698,22 @@ func (h *jiraHandler) transition(ctx context.Context, db *DB, c jiraConfig, ev E
|
||||
if card.JiraKey == "" {
|
||||
return h.create(ctx, db, c, ev)
|
||||
}
|
||||
target, ok := c.StatusMap[card.ColumnName]
|
||||
if !ok || target == "" {
|
||||
if _, ok := c.StatusMap[card.ColumnName]; !ok {
|
||||
return 0, fmt.Errorf("no status_map entry for column %q", card.ColumnName)
|
||||
}
|
||||
status, body, err := h.jiraRequest(ctx, c, http.MethodGet, "/rest/api/3/issue/"+card.JiraKey+"/transitions", nil)
|
||||
return h.transitionToStatus(ctx, c, card.JiraKey, card.ColumnName)
|
||||
}
|
||||
|
||||
// transitionToStatus drives a Jira issue to the status mapped from the given
|
||||
// kanban column and refreshes labels accordingly. Used by transition() on
|
||||
// card.moved events and by create() right after issue creation so new issues
|
||||
// do not stall at the workflow's default initial status.
|
||||
func (h *jiraHandler) transitionToStatus(ctx context.Context, c jiraConfig, jiraKey, columnName string) (int, error) {
|
||||
target := c.StatusMap[columnName]
|
||||
if target == "" {
|
||||
return 0, fmt.Errorf("no status_map entry for column %q", columnName)
|
||||
}
|
||||
status, body, err := h.jiraRequest(ctx, c, http.MethodGet, "/rest/api/3/issue/"+jiraKey+"/transitions", nil)
|
||||
if err != nil {
|
||||
return status, err
|
||||
}
|
||||
@@ -704,11 +737,25 @@ func (h *jiraHandler) transition(ctx context.Context, db *DB, c jiraConfig, ev E
|
||||
}
|
||||
}
|
||||
if tID == "" {
|
||||
return 0, fmt.Errorf("transition %q not available for %s", target, card.JiraKey)
|
||||
return 0, fmt.Errorf("transition %q not available for %s", target, jiraKey)
|
||||
}
|
||||
req := map[string]interface{}{"transition": map[string]string{"id": tID}}
|
||||
status, _, err = h.jiraRequest(ctx, c, http.MethodPost, "/rest/api/3/issue/"+card.JiraKey+"/transitions", req)
|
||||
return status, err
|
||||
status, _, err = h.jiraRequest(ctx, c, http.MethodPost, "/rest/api/3/issue/"+jiraKey+"/transitions", req)
|
||||
if err != nil {
|
||||
return status, err
|
||||
}
|
||||
// Refresh labels to match the new column. Replaces the labels array; an
|
||||
// empty list strips any stale labels from the previous column.
|
||||
labels := c.LabelsMap[columnName]
|
||||
if labels == nil {
|
||||
labels = []string{}
|
||||
}
|
||||
lbody := map[string]interface{}{"fields": map[string]interface{}{"labels": labels}}
|
||||
lStatus, _, lErr := h.jiraRequest(ctx, c, http.MethodPut, "/rest/api/3/issue/"+jiraKey, lbody)
|
||||
if lErr != nil {
|
||||
return lStatus, fmt.Errorf("transition ok but labels sync failed: %w", lErr)
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (h *jiraHandler) comment(ctx context.Context, db *DB, c jiraConfig, ev Event) (int, error) {
|
||||
|
||||
Reference in New Issue
Block a user