Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6d401dc
Added jira on issue created trigger component
alabro-bm Feb 5, 2026
039408e
feat: add Jira OAuth, on ticket created, webhooks, component mappers,…
alabro-bm Feb 9, 2026
b5956bc
Added additional tests, code cleanup
alabro-bm Feb 9, 2026
927861d
Merge branch 'main' into feature/superplane-2890-on-create-issue
alabro-bm Feb 9, 2026
d6c533d
Updated to new webhook core impl, regen docs
alabro-bm Feb 9, 2026
42b0eb2
Merge branch 'main' into feature/superplane-2890-on-create-issue
alabro-bm Feb 9, 2026
39b1a1e
Formatting fix
alabro-bm Feb 9, 2026
2bdeeae
Fixed the jira test issue
alabro-bm Feb 9, 2026
c32c151
Removed webhook debugging compoents
alabro-bm Feb 9, 2026
e40eb84
Updated jira tests
alabro-bm Feb 9, 2026
689ae0b
Merge branch 'main' into feature/superplane-2890-on-create-issue
alabro-bm Feb 9, 2026
8fbe0fc
Updated jira icon
alabro-bm Feb 10, 2026
7d32781
Merge branch 'main' into feature/superplane-2890-on-create-issue
alabro-bm Feb 10, 2026
7542a05
Updated the webhook handler for jira with the new core impl
alabro-bm Feb 10, 2026
f06a3c5
Merge branch 'main' into feature/superplane-2890-on-create-issue
alabro-bm Feb 12, 2026
fdb1a9b
Merge branch 'main' into feature/superplane-2890-on-create-issue
shiroyasha Feb 13, 2026
4e05f59
Added instructions for jira setup
alabro-bm Feb 13, 2026
ffe64e6
Cursor comments fix
alabro-bm Feb 16, 2026
0a38e78
Updated tests and docs
alabro-bm Feb 16, 2026
ffb53d1
Code cleanup
alabro-bm Feb 16, 2026
90a8567
Merge branch 'main' into feature/superplane-2890-on-create-issue
shiroyasha Feb 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions docs/components/Jira.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ title: "Jira"

Manage and react to issues in Jira

## Triggers

<CardGrid>
<LinkCard title="On Issue Created" href="#on-issue-created" description="Listen for new issues created in Jira" />
</CardGrid>

import { CardGrid, LinkCard } from "@astrojs/starlight/components";

## Actions
Expand All @@ -12,6 +18,112 @@ import { CardGrid, LinkCard } from "@astrojs/starlight/components";
<LinkCard title="Create Issue" href="#create-issue" description="Create a new issue in Jira" />
</CardGrid>

## Instructions

Jira supports two authentication methods. Choose the one that fits your use case:

## API Token

Use this method for simple setups that only need issue management (no webhooks).

1. Go to **Atlassian API Tokens** (https://id.atlassian.com/manage-profile/security/api-tokens)
2. Click **Create API token** and copy the token
3. Select **API Token** as auth type below and enter your Jira base URL, email, and token

## OAuth 2.0

Use this method if you need webhook support (e.g. On Issue Created trigger).

1. Go to **Atlassian Developer Console** (https://developer.atlassian.com/console/myapps/)
2. Click **Create** > **OAuth 2.0 integration**, name your app and agree to the terms
3. Go to **Permissions** and add these scopes under **Jira API**:
- read:jira-work - Read issues and projects
- write:jira-work - Create issues
- read:jira-user - Read user info
- manage:jira-webhook - Register and delete webhooks
4. Go to **Distribution**, click **Edit**, and set the status to **Sharing** to allow other users in your organization to connect
5. Go to **Settings** to find the **Client ID** and create a **Secret**
6. Select **OAuth 2.0** as auth type below and enter the Client ID and Client Secret
7. After creating the integration, go to the Atlassian app **Authorization** > **OAuth 2.0 (3LO)** and set the callback URL to your SuperPlane integration callback URL
8. Click **Connect** to authorize with Atlassian

<a id="on-issue-created"></a>

## On Issue Created

The On Issue Created trigger starts a workflow execution when a new issue is created in a Jira project.

### Use Cases

- **Issue automation**: Automate responses to new issues
- **Notification workflows**: Send notifications when issues are created
- **Task management**: Sync issues with external task management systems
- **Triage automation**: Automatically categorize or assign new issues

### Configuration

- **Project**: Select the Jira project to monitor
- **Issue Types**: Optionally filter by issue type (Task, Bug, Story, etc.)

### Event Data

Each issue created event includes:
- **webhookEvent**: The event type (jira:issue_created)
- **issue**: Complete issue information including key, summary, fields
- **user**: User who created the issue

### Webhook Setup

This trigger automatically sets up a Jira webhook when configured. The webhook is managed by SuperPlane and will be cleaned up when the trigger is removed.

### Example Data

```json
{
"data": {
"issue": {
"fields": {
"creator": {
"displayName": "John Doe"
},
"issuetype": {
"id": "10001",
"name": "Task"
},
"priority": {
"name": "Medium"
},
"project": {
"id": "10000",
"key": "PROJ",
"name": "My Project"
},
"reporter": {
"displayName": "John Doe"
},
"status": {
"name": "To Do"
},
"summary": "Implement new feature"
},
"id": "10001",
"key": "PROJ-123",
"self": "https://your-domain.atlassian.net/rest/api/3/issue/10001"
},
"issue_event_type_name": "issue_created",
"timestamp": 1612345678901,
"user": {
"accountId": "557058:abcd1234-5678-efgh-ijkl-mnopqrstuvwx",
"displayName": "John Doe",
"emailAddress": "john.doe@example.com"
},
"webhookEvent": "jira:issue_created"
},
"timestamp": "2026-01-19T12:00:00Z",
"type": "jira.issueCreated"
}
```

<a id="create-issue"></a>

## Create Issue
Expand Down
190 changes: 180 additions & 10 deletions pkg/integrations/jira/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,29 @@ import (
)

type Client struct {
Email string
Token string
AuthType string
// For API Token auth
Email string
Token string
// For OAuth auth
AccessToken string
CloudID string
// Common
BaseURL string
http core.HTTPContext
}

func NewClient(httpCtx core.HTTPContext, ctx core.IntegrationContext) (*Client, error) {
authType, _ := ctx.GetConfig("authType")

if string(authType) == AuthTypeOAuth {
return newOAuthClient(httpCtx, ctx)
}

return newAPITokenClient(httpCtx, ctx)
}

func newAPITokenClient(httpCtx core.HTTPContext, ctx core.IntegrationContext) (*Client, error) {
baseURL, err := ctx.GetConfig("baseUrl")
if err != nil {
return nil, fmt.Errorf("error getting baseUrl: %v", err)
Expand All @@ -35,18 +51,61 @@ func NewClient(httpCtx core.HTTPContext, ctx core.IntegrationContext) (*Client,
}

return &Client{
Email: string(email),
Token: string(apiToken),
BaseURL: string(baseURL),
http: httpCtx,
AuthType: AuthTypeAPIToken,
Email: string(email),
Token: string(apiToken),
BaseURL: string(baseURL),
http: httpCtx,
}, nil
}

func newOAuthClient(httpCtx core.HTTPContext, ctx core.IntegrationContext) (*Client, error) {
accessToken, err := findOAuthSecret(ctx, OAuthAccessToken)
if err != nil {
return nil, fmt.Errorf("error getting access token: %v", err)
}

if accessToken == "" {
return nil, fmt.Errorf("OAuth access token not found")
}

metadata := ctx.GetMetadata()
metadataMap, ok := metadata.(map[string]any)
if !ok {
return nil, fmt.Errorf("invalid metadata format")
}

cloudID, _ := metadataMap["cloudId"].(string)
if cloudID == "" {
return nil, fmt.Errorf("cloud ID not found in metadata")
}

return &Client{
AuthType: AuthTypeOAuth,
AccessToken: accessToken,
CloudID: cloudID,
http: httpCtx,
}, nil
}

func (c *Client) authHeader() string {
if c.AuthType == AuthTypeOAuth {
return fmt.Sprintf("Bearer %s", c.AccessToken)
}
credentials := fmt.Sprintf("%s:%s", c.Email, c.Token)
return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(credentials)))
}

func (c *Client) apiURL(path string) string {
var url string
if c.AuthType == AuthTypeOAuth {
url = fmt.Sprintf("https://api.atlassian.com/ex/jira/%s%s", c.CloudID, path)
} else {
url = fmt.Sprintf("%s%s", c.BaseURL, path)
}
return url
}

func (c *Client) execRequest(method, url string, body io.Reader) ([]byte, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
Expand Down Expand Up @@ -84,7 +143,7 @@ type User struct {

// GetCurrentUser verifies credentials by fetching the authenticated user.
func (c *Client) GetCurrentUser() (*User, error) {
url := fmt.Sprintf("%s/rest/api/3/myself", c.BaseURL)
url := c.apiURL("/rest/api/3/myself")
responseBody, err := c.execRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
Expand All @@ -107,7 +166,7 @@ type Project struct {

// ListProjects returns all projects accessible to the authenticated user.
func (c *Client) ListProjects() ([]Project, error) {
url := fmt.Sprintf("%s/rest/api/3/project", c.BaseURL)
url := c.apiURL("/rest/api/3/project")
responseBody, err := c.execRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
Expand All @@ -131,7 +190,7 @@ type Issue struct {

// GetIssue fetches a single issue by its key.
func (c *Client) GetIssue(issueKey string) (*Issue, error) {
url := fmt.Sprintf("%s/rest/api/3/issue/%s", c.BaseURL, issueKey)
url := c.apiURL(fmt.Sprintf("/rest/api/3/issue/%s", issueKey))
responseBody, err := c.execRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
Expand Down Expand Up @@ -215,7 +274,7 @@ type CreateIssueResponse struct {

// CreateIssue creates a new issue in Jira.
func (c *Client) CreateIssue(req *CreateIssueRequest) (*CreateIssueResponse, error) {
url := fmt.Sprintf("%s/rest/api/3/issue", c.BaseURL)
url := c.apiURL("/rest/api/3/issue")

body, err := json.Marshal(req)
if err != nil {
Expand All @@ -234,3 +293,114 @@ func (c *Client) CreateIssue(req *CreateIssueRequest) (*CreateIssueResponse, err

return &response, nil
}

// WebhookRegistrationRequest is the request body for registering webhooks.
type WebhookRegistrationRequest struct {
URL string `json:"url"`
Webhooks []WebhookSpec `json:"webhooks"`
}

// WebhookSpec defines a single webhook configuration.
type WebhookSpec struct {
JQLFilter string `json:"jqlFilter"`
Events []string `json:"events"`
}

// WebhookRegistrationResponse is the response from registering webhooks.
type WebhookRegistrationResponse struct {
WebhookRegistrationResult []WebhookRegistrationResult `json:"webhookRegistrationResult"`
}

// WebhookRegistrationResult contains the result of a single webhook registration.
type WebhookRegistrationResult struct {
CreatedWebhookID int64 `json:"createdWebhookId"`
Errors []string `json:"errors,omitempty"`
}

// RegisterWebhook registers a new webhook in Jira.
func (c *Client) RegisterWebhook(webhookURL, jqlFilter string, events []string) (*WebhookRegistrationResponse, error) {
url := c.apiURL("/rest/api/3/webhook")

req := WebhookRegistrationRequest{
URL: webhookURL,
Webhooks: []WebhookSpec{
{
JQLFilter: jqlFilter,
Events: events,
},
},
}

body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("error marshaling request: %v", err)
}

responseBody, err := c.execRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return nil, err
}

var response WebhookRegistrationResponse
if err := json.Unmarshal(responseBody, &response); err != nil {
return nil, fmt.Errorf("error parsing webhook registration response: %v", err)
}

return &response, nil
}

// FailedWebhookResponse is the response from the failed webhooks endpoint.
type FailedWebhookResponse struct {
Values []FailedWebhook `json:"values"`
}

// FailedWebhook contains information about a failed webhook delivery.
type FailedWebhook struct {
ID string `json:"id"`
Body string `json:"body"`
URL string `json:"url"`
FailureReason string `json:"failureReason"`
LatestFailureTime string `json:"latestFailureTime"`
}

// GetFailedWebhooks returns webhooks that failed to be delivered in the last 72 hours.
func (c *Client) GetFailedWebhooks() (*FailedWebhookResponse, error) {
url := c.apiURL("/rest/api/3/webhook/failed")
responseBody, err := c.execRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}

var response FailedWebhookResponse
if err := json.Unmarshal(responseBody, &response); err != nil {
return nil, fmt.Errorf("error parsing failed webhooks response: %v", err)
}

return &response, nil
}

// WebhookDeletionRequest is the request body for deleting webhooks.
type WebhookDeletionRequest struct {
WebhookIDs []int64 `json:"webhookIds"`
}

// DeleteWebhook removes webhooks from Jira.
func (c *Client) DeleteWebhook(webhookIDs []int64) error {
url := c.apiURL("/rest/api/3/webhook")

req := WebhookDeletionRequest{
WebhookIDs: webhookIDs,
}

body, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("error marshaling request: %v", err)
}

_, err = c.execRequest(http.MethodDelete, url, bytes.NewReader(body))
if err != nil {
return err
}

return nil
}
Loading