Jira Provider

Jira Provider

The Jira provider integrates with Atlassian Jira for issue tracking, project management, and workflow automation.

Installation

go get github.com/resolute/resolute/providers/jira

Configuration

JiraConfig

type JiraConfig struct {
    BaseURL   string  // Jira instance URL (e.g., "https://company.atlassian.net")
    Email     string  // User email for authentication
    APIToken  string  // API token for authentication
}

Environment Variables

VariableDescriptionRequired
JIRA_BASE_URLJira instance URLYes
JIRA_EMAILAuthentication emailYes
JIRA_API_TOKENAPI tokenYes

Provider Constructor

NewProvider

func NewProvider(cfg JiraConfig) *JiraProvider

Creates a new Jira provider with the given configuration.

Parameters:

  • cfg - Jira configuration

Returns: *JiraProvider implementing core.Provider

Example:

provider := jira.NewProvider(jira.JiraConfig{
    BaseURL:  os.Getenv("JIRA_BASE_URL"),
    Email:    os.Getenv("JIRA_EMAIL"),
    APIToken: os.Getenv("JIRA_API_TOKEN"),
})

// Use with worker
core.NewWorker().
    WithConfig(cfg).
    WithFlow(flow).
    WithProviders(provider).
    Run()

Types

Issue

type Issue struct {
    Key         string            `json:"key"`
    ID          string            `json:"id"`
    Summary     string            `json:"summary"`
    Description string            `json:"description"`
    Status      string            `json:"status"`
    Priority    string            `json:"priority"`
    Assignee    string            `json:"assignee"`
    Reporter    string            `json:"reporter"`
    Labels      []string          `json:"labels"`
    Components  []string          `json:"components"`
    Created     time.Time         `json:"created"`
    Updated     time.Time         `json:"updated"`
    Fields      map[string]any    `json:"fields"`
}

Transition

type Transition struct {
    ID   string `json:"id"`
    Name string `json:"name"`
    To   string `json:"to"`
}

Comment

type Comment struct {
    ID      string    `json:"id"`
    Body    string    `json:"body"`
    Author  string    `json:"author"`
    Created time.Time `json:"created"`
}

Activities

FetchIssues

Fetches issues from Jira using JQL (Jira Query Language).

Input:

type FetchInput struct {
    JQL        string    `json:"jql"`         // JQL query string
    Project    string    `json:"project"`     // Project key (alternative to JQL)
    MaxResults int       `json:"max_results"` // Max issues to return (default: 50)
    StartAt    int       `json:"start_at"`    // Pagination offset
    Fields     []string  `json:"fields"`      // Fields to include
    Cursor     string    `json:"cursor"`      // For incremental sync (RFC3339 timestamp)
}

Output:

type FetchOutput struct {
    Issues     []Issue `json:"issues"`
    Total      int     `json:"total"`
    Count      int     `json:"count"`
    StartAt    int     `json:"start_at"`
    HasMore    bool    `json:"has_more"`
    NextCursor string  `json:"next_cursor"`
}

Node Factory:

func FetchIssues(input FetchInput) *core.Node[FetchInput, FetchOutput]

Example:

// Fetch all issues from a project
fetchNode := jira.FetchIssues(jira.FetchInput{
    Project:    "PLATFORM",
    MaxResults: 100,
})

// Fetch with JQL
fetchNode := jira.FetchIssues(jira.FetchInput{
    JQL: "project = PLATFORM AND status = 'In Progress'",
})

// Incremental sync with cursor
fetchNode := jira.FetchIssues(jira.FetchInput{
    JQL:    "project = PLATFORM",
    Cursor: core.CursorFor("jira"),
})

FetchIssue

Fetches a single issue by key.

Input:

type FetchIssueInput struct {
    Key    string   `json:"key"`    // Issue key (e.g., "PROJ-123")
    Fields []string `json:"fields"` // Fields to include
}

Output:

type FetchIssueOutput struct {
    Issue Issue `json:"issue"`
}

Node Factory:

func FetchIssue(input FetchIssueInput) *core.Node[FetchIssueInput, FetchIssueOutput]

Example:

fetchNode := jira.FetchIssue(jira.FetchIssueInput{
    Key: "PLATFORM-123",
})

SearchJQL

Executes a JQL search query with full control over parameters.

Input:

type SearchInput struct {
    JQL        string   `json:"jql"`
    MaxResults int      `json:"max_results"`
    StartAt    int      `json:"start_at"`
    Fields     []string `json:"fields"`
    Expand     []string `json:"expand"`
}

Output:

type SearchOutput struct {
    Issues     []Issue `json:"issues"`
    Total      int     `json:"total"`
    MaxResults int     `json:"max_results"`
    StartAt    int     `json:"start_at"`
}

Node Factory:

func SearchJQL(input SearchInput) *core.Node[SearchInput, SearchOutput]

Example:

searchNode := jira.SearchJQL(jira.SearchInput{
    JQL:        "project = PLATFORM AND updated >= -7d",
    MaxResults: 50,
    Fields:     []string{"summary", "status", "assignee"},
})

CreateIssue

Creates a new issue in Jira.

Input:

type CreateInput struct {
    Project     string         `json:"project"`
    IssueType   string         `json:"issue_type"`
    Summary     string         `json:"summary"`
    Description string         `json:"description"`
    Priority    string         `json:"priority"`
    Assignee    string         `json:"assignee"`
    Labels      []string       `json:"labels"`
    Components  []string       `json:"components"`
    CustomFields map[string]any `json:"custom_fields"`
}

Output:

type CreateOutput struct {
    Key string `json:"key"`
    ID  string `json:"id"`
}

Node Factory:

func CreateIssue(input CreateInput) *core.Node[CreateInput, CreateOutput]

Example:

createNode := jira.CreateIssue(jira.CreateInput{
    Project:     "PLATFORM",
    IssueType:   "Bug",
    Summary:     "Authentication failing on login",
    Description: "Users cannot log in with valid credentials",
    Priority:    "High",
    Labels:      []string{"auth", "urgent"},
})

UpdateIssue

Updates an existing issue.

Input:

type UpdateInput struct {
    Key         string         `json:"key"`
    Summary     string         `json:"summary"`
    Description string         `json:"description"`
    Priority    string         `json:"priority"`
    Assignee    string         `json:"assignee"`
    Labels      []string       `json:"labels"`
    CustomFields map[string]any `json:"custom_fields"`
}

Output:

type UpdateOutput struct {
    Key     string `json:"key"`
    Updated bool   `json:"updated"`
}

Node Factory:

func UpdateIssue(input UpdateInput) *core.Node[UpdateInput, UpdateOutput]

Example:

updateNode := jira.UpdateIssue(jira.UpdateInput{
    Key:      "PLATFORM-123",
    Priority: "Critical",
    Labels:   []string{"auth", "urgent", "escalated"},
})

TransitionIssue

Transitions an issue to a new status.

Input:

type TransitionInput struct {
    Key          string         `json:"key"`
    TransitionID string         `json:"transition_id"`
    Comment      string         `json:"comment"`
    Fields       map[string]any `json:"fields"`
}

Output:

type TransitionOutput struct {
    Key       string `json:"key"`
    NewStatus string `json:"new_status"`
}

Node Factory:

func TransitionIssue(input TransitionInput) *core.Node[TransitionInput, TransitionOutput]

Example:

transitionNode := jira.TransitionIssue(jira.TransitionInput{
    Key:          "PLATFORM-123",
    TransitionID: "31", // "Done" transition
    Comment:      "Fixed in release v2.1.0",
})

AddComment

Adds a comment to an issue.

Input:

type CommentInput struct {
    Key  string `json:"key"`
    Body string `json:"body"`
}

Output:

type CommentOutput struct {
    CommentID string `json:"comment_id"`
}

Node Factory:

func AddComment(input CommentInput) *core.Node[CommentInput, CommentOutput]

Example:

commentNode := jira.AddComment(jira.CommentInput{
    Key:  "PLATFORM-123",
    Body: "Deployment completed successfully",
})

GetTransitions

Gets available transitions for an issue.

Input:

type GetTransitionsInput struct {
    Key string `json:"key"`
}

Output:

type GetTransitionsOutput struct {
    Transitions []Transition `json:"transitions"`
}

Node Factory:

func GetTransitions(input GetTransitionsInput) *core.Node[GetTransitionsInput, GetTransitionsOutput]

Usage Patterns

Basic Issue Sync Flow

flow := core.NewFlow("jira-sync").
    TriggeredBy(core.Schedule("0 * * * *")).
    Then(jira.FetchIssues(jira.FetchInput{
        Project:    "PLATFORM",
        Cursor:     core.CursorFor("jira"),
        MaxResults: 100,
    }).As("issues")).
    When(func(s *core.FlowState) bool {
        result := core.Get[jira.FetchOutput](s, "issues")
        return result.Count > 0
    }).
        Then(processIssuesNode).
    EndWhen().
    Build()

Issue Creation with Compensation

createIssue := jira.CreateIssue(jira.CreateInput{
    Project:   "PLATFORM",
    IssueType: "Task",
    Summary:   core.Output("request.title"),
}).OnError(jira.TransitionIssue(jira.TransitionInput{
    Key:          core.Output("create-issue.Key"),
    TransitionID: "cancel",
}))

flow := core.NewFlow("create-with-rollback").
    TriggeredBy(core.Manual("api")).
    Then(createIssue.As("create-issue")).
    Then(assignResourcesNode).
    Build()

Rate-Limited Bulk Operations

// Provider-level rate limiting
provider := jira.NewProvider(cfg).WithRateLimit(100, time.Minute)

// Or per-node rate limiting
fetchNode := jira.FetchIssues(input).WithRateLimit(50, time.Minute)

Pagination Flow

flow := core.NewFlow("paginated-fetch").
    TriggeredBy(core.Manual("api")).
    Then(jira.SearchJQL(jira.SearchInput{
        JQL:        "project = PLATFORM",
        MaxResults: 50,
    }).As("page")).
    While(func(s *core.FlowState) bool {
        result := core.Get[jira.SearchOutput](s, "page")
        return result.StartAt + len(result.Issues) < result.Total
    }).
        Then(processPageNode).
        Then(nextPageNode).
    EndWhile().
    Build()

Complete Example

package main

import (
    "os"
    "time"

    "github.com/resolute/resolute/core"
    "github.com/resolute/resolute/providers/jira"
)

func main() {
    // Configure provider
    jiraProvider := jira.NewProvider(jira.JiraConfig{
        BaseURL:  os.Getenv("JIRA_BASE_URL"),
        Email:    os.Getenv("JIRA_EMAIL"),
        APIToken: os.Getenv("JIRA_API_TOKEN"),
    }).WithRateLimit(100, time.Minute)

    // Build flow
    flow := core.NewFlow("jira-issue-processor").
        TriggeredBy(core.Schedule("0 */2 * * *")).
        Then(jira.FetchIssues(jira.FetchInput{
            JQL:        "project = PLATFORM AND status = 'To Do'",
            Cursor:     core.CursorFor("jira-todo"),
            MaxResults: 50,
        }).As("issues")).
        When(func(s *core.FlowState) bool {
            issues := core.Get[jira.FetchOutput](s, "issues")
            return issues.Count > 0
        }).
            Then(triageIssuesNode).
            Then(notifyTeamNode).
        EndWhen().
        Build()

    // Run worker
    err := core.NewWorker().
        WithConfig(core.WorkerConfig{
            TaskQueue: "jira-processor",
        }).
        WithFlow(flow).
        WithProviders(jiraProvider).
        Run()

    if err != nil {
        panic(err)
    }
}

See Also