From 375069ca28b276f9c477b6b3874e71d5755bca6d Mon Sep 17 00:00:00 2001 From: Gopal Date: Sat, 3 Jan 2026 15:13:47 +0530 Subject: [PATCH 1/6] feat: Add Jira platform support to daemon serve command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add JiraPlatformConfig struct and registration logic in serve.rs - Export JiraPlatform and JiraConfig from aof-triggers lib - Support both direct secret and HMAC-SHA256 signature verification - Handle sha256= prefix in webhook signatures Documentation updates: - Add Jira platform section to daemon-config.md reference - Update jira-integration.md with correct DaemonConfig format - Add webhook payload templates for Jira Automation rules - Document both Automation Rules and System Webhooks setup options - Add payload templates for: issue created/updated, comment, worklog, sprint events 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/aof-triggers/src/lib.rs | 1 + crates/aof-triggers/src/platforms/jira.rs | 29 ++- crates/aofctl/src/commands/serve.rs | 100 +++++++++ docs/concepts/jira-integration.md | 50 +++-- docs/reference/daemon-config.md | 62 ++++++ docs/reference/jira-integration.md | 244 ++++++++++++++++------ docs/tutorials/jira-automation.md | 212 +++++++++++++++++-- 7 files changed, 595 insertions(+), 103 deletions(-) diff --git a/crates/aof-triggers/src/lib.rs b/crates/aof-triggers/src/lib.rs index 70a6c0e..454b632 100644 --- a/crates/aof-triggers/src/lib.rs +++ b/crates/aof-triggers/src/lib.rs @@ -54,6 +54,7 @@ pub use platforms::{ TelegramConfig, TelegramPlatform, WhatsAppConfig, WhatsAppPlatform, GitHubConfig, GitHubPlatform, + JiraConfig, JiraPlatform, PagerDutyConfig, PagerDutyPlatform, TypedPlatformConfig, // Platform registry for extensibility diff --git a/crates/aof-triggers/src/platforms/jira.rs b/crates/aof-triggers/src/platforms/jira.rs index 88272b7..e816c59 100644 --- a/crates/aof-triggers/src/platforms/jira.rs +++ b/crates/aof-triggers/src/platforms/jira.rs @@ -431,8 +431,25 @@ impl JiraPlatform { Ok(Self { config, client }) } - /// Verify HMAC-SHA256 signature from Jira webhook + /// Verify signature from Jira webhook + /// Supports multiple modes: + /// 1. HMAC-SHA256 signature (prefixed with "sha256=" or raw hex) + /// 2. Static shared secret (direct comparison for Jira Automation) fn verify_jira_signature(&self, payload: &[u8], signature: &str) -> bool { + // Strip common prefixes like "sha256=" or "sha1=" if present + let provided_signature = signature + .strip_prefix("sha256=") + .or_else(|| signature.strip_prefix("sha1=")) + .unwrap_or(signature); + + // Mode 1: Direct secret comparison (for Jira Automation static secrets) + // Jira Automation sends the secret value directly in the header + if provided_signature == self.config.webhook_secret { + debug!("Jira signature verified via direct secret match"); + return true; + } + + // Mode 2: HMAC-SHA256 verification (for computed signatures) let mut mac = match HmacSha256::new_from_slice(self.config.webhook_secret.as_bytes()) { Ok(m) => m, Err(e) => { @@ -445,14 +462,14 @@ impl JiraPlatform { let result = mac.finalize(); let computed_signature = hex::encode(result.into_bytes()); - if computed_signature == signature { - debug!("Jira signature verified successfully"); + if computed_signature == provided_signature { + debug!("Jira signature verified via HMAC-SHA256"); true } else { debug!( - "Signature mismatch - computed: {}, provided: {}", - &computed_signature[..8], - &signature[..8.min(signature.len())] + "Signature mismatch - computed HMAC: {}..., provided: {}...", + &computed_signature[..8.min(computed_signature.len())], + &provided_signature[..8.min(provided_signature.len())] ); false } diff --git a/crates/aofctl/src/commands/serve.rs b/crates/aofctl/src/commands/serve.rs index 9103f93..923e39d 100644 --- a/crates/aofctl/src/commands/serve.rs +++ b/crates/aofctl/src/commands/serve.rs @@ -18,6 +18,7 @@ use aof_triggers::{ TelegramPlatform, TelegramConfig, WhatsAppPlatform, WhatsAppConfig, GitHubPlatform, GitHubConfig, + JiraPlatform, JiraConfig, CommandBinding as HandlerCommandBinding, flow::{FlowRegistry, FlowRouter}, }; @@ -135,6 +136,9 @@ pub struct PlatformConfigs { /// WhatsApp configuration pub whatsapp: Option, + + /// Jira configuration + pub jira: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -203,6 +207,43 @@ pub struct WhatsAppPlatformConfig { pub app_secret: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JiraPlatformConfig { + #[serde(default = "default_true")] + pub enabled: bool, + + /// Jira Cloud ID (for cloud instances) + pub cloud_id: Option, + pub cloud_id_env: Option, + + /// Base URL (e.g., https://your-domain.atlassian.net) + pub base_url: Option, + + /// User email for API authentication + pub user_email: Option, + pub user_email_env: Option, + + /// API token for authentication + pub api_token: Option, + pub api_token_env: Option, + + /// Webhook secret for signature verification + pub webhook_secret: Option, + pub webhook_secret_env: Option, + + /// Bot name for identification in comments + #[serde(default)] + pub bot_name: Option, + + /// Allowed project keys (whitelist) + #[serde(default)] + pub allowed_projects: Option>, + + /// Allowed event types (whitelist) + #[serde(default)] + pub allowed_events: Option>, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct AgentDiscoveryConfig { /// Directory containing agent YAML files @@ -512,6 +553,65 @@ pub async fn execute( } } + // Jira + if let Some(jira_config) = &config.spec.platforms.jira { + if jira_config.enabled { + let api_token = resolve_env_value( + jira_config.api_token.as_deref(), + jira_config.api_token_env.as_deref(), + ); + let user_email = resolve_env_value( + jira_config.user_email.as_deref(), + jira_config.user_email_env.as_deref(), + ); + let webhook_secret = resolve_env_value( + jira_config.webhook_secret.as_deref(), + jira_config.webhook_secret_env.as_deref(), + ); + + // Build base URL from cloud_id or use provided base_url + let base_url = if let Some(ref url) = jira_config.base_url { + Some(url.clone()) + } else { + let cloud_id = resolve_env_value( + jira_config.cloud_id.as_deref(), + jira_config.cloud_id_env.as_deref(), + ); + cloud_id.map(|id| format!("https://api.atlassian.com/ex/jira/{}", id)) + }; + + if let (Some(token), Some(email), Some(secret), Some(url)) = + (api_token, user_email, webhook_secret, base_url) + { + let platform_config = JiraConfig { + base_url: url, + email, + api_token: token, + webhook_secret: secret, + bot_name: jira_config.bot_name.clone().unwrap_or_else(|| "aofbot".to_string()), + allowed_projects: jira_config.allowed_projects.clone(), + allowed_events: jira_config.allowed_events.clone(), + allowed_users: None, + enable_comments: true, + enable_updates: true, + enable_transitions: true, + }; + match JiraPlatform::new(platform_config) { + Ok(platform) => { + handler.register_platform(Arc::new(platform)); + println!(" Registered platform: jira"); + platforms_registered += 1; + } + Err(e) => { + eprintln!(" Failed to create Jira platform: {}", e); + } + } + } else { + eprintln!(" Jira enabled but missing required config (api_token, user_email, webhook_secret, and base_url or cloud_id)"); + } + } + } + // Load Triggers from directory let triggers_dir_path = triggers_dir .map(PathBuf::from) diff --git a/docs/concepts/jira-integration.md b/docs/concepts/jira-integration.md index df66e86..d1dc7c5 100644 --- a/docs/concepts/jira-integration.md +++ b/docs/concepts/jira-integration.md @@ -404,39 +404,53 @@ spec: ```yaml # daemon.yaml -apiVersion: aof.dev/v1alpha1 +apiVersion: aof.dev/v1 kind: DaemonConfig metadata: name: aof-daemon spec: server: - host: 0.0.0.0 + host: "0.0.0.0" port: 3000 + cors: true + timeout_secs: 60 platforms: - - type: Jira - config: - webhook_secret: ${JIRA_WEBHOOK_SECRET} - webhook_path: /webhook/jira # Default path + jira: + enabled: true + # Use base_url for direct Atlassian URL (recommended) + base_url: https://your-domain.atlassian.net + # Authentication credentials via environment variables + user_email_env: JIRA_USER_EMAIL + api_token_env: JIRA_API_TOKEN + webhook_secret_env: JIRA_WEBHOOK_SECRET + bot_name: aofbot # Optional: name displayed in comments + + # Optional: Restrict to specific projects + allowed_projects: + - PROJ + - DEV - # Optional: Filter at platform level - allowed_projects: - - PROJ - - DEV + # Resource directories + triggers: + directory: "./triggers" + watch: true - # Jira API credentials for agent actions - api_config: - instance_url: ${JIRA_CLOUD_INSTANCE_URL} - user_email: ${JIRA_USER_EMAIL} - api_token: ${JIRA_API_TOKEN} + agents: + directory: "./agents" flows: - - path: flows/bug-triage.yaml - - path: flows/sprint-planning.yaml - - path: flows/standup-summary.yaml + directory: "./flows" + enabled: true + + runtime: + max_concurrent_tasks: 10 + task_timeout_secs: 300 ``` +**Webhook endpoint**: `https://your-domain.com/webhook/jira` + ### Trigger with Interactive Commands Enable `/analyze` style commands in Jira comments: diff --git a/docs/reference/daemon-config.md b/docs/reference/daemon-config.md index 9a069c8..718f0d3 100644 --- a/docs/reference/daemon-config.md +++ b/docs/reference/daemon-config.md @@ -25,6 +25,7 @@ spec: telegram: object discord: object whatsapp: object + jira: object agents: # Required: Agent discovery directory: string fleets: # Optional: Fleet discovery @@ -167,6 +168,65 @@ spec: verify_token_env: WHATSAPP_VERIFY_TOKEN ``` +### Jira Platform + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `enabled` | bool | Yes | Enable Jira Cloud integration | +| `base_url` | string | Yes* | Jira instance URL (e.g., `https://your-domain.atlassian.net`) | +| `cloud_id_env` | string | Yes* | Env var for Jira Cloud ID (alternative to base_url) | +| `user_email_env` | string | Yes | Env var for user email for API authentication | +| `api_token_env` | string | Yes | Env var for API token | +| `webhook_secret_env` | string | Yes | Env var for webhook secret (for signature verification) | +| `bot_name` | string | No | Bot name for comments (default: "aofbot") | +| `allowed_projects` | array | No | Project keys allowed to trigger (whitelist) | +| `allowed_events` | array | No | Event types to handle (whitelist) | + +*Either `base_url` or `cloud_id_env` must be provided. + +**Supported Events:** +- `jira:issue_created` - Issue created +- `jira:issue_updated` - Issue updated +- `jira:issue_deleted` - Issue deleted +- `comment_created` - Comment added +- `comment_updated` - Comment updated +- `comment_deleted` - Comment deleted +- `sprint_started` - Sprint started +- `sprint_closed` - Sprint closed +- `worklog_created` - Work logged +- `worklog_updated` - Worklog updated + +**Example:** +```yaml +spec: + platforms: + jira: + enabled: true + base_url: https://your-domain.atlassian.net + user_email_env: JIRA_USER_EMAIL + api_token_env: JIRA_API_TOKEN + webhook_secret_env: JIRA_WEBHOOK_SECRET + bot_name: aof-automation + + # Optional: Restrict to specific projects + allowed_projects: + - SCRUM + - OPS + + # Optional: Only handle these events + allowed_events: + - jira:issue_created + - jira:issue_updated + - comment_created +``` + +**Setting up Jira Automation webhook URL:** + +Configure your Jira Automation rules to POST to: +``` +https://your-domain/webhook/jira +``` + --- ## Agent Discovery @@ -379,6 +439,7 @@ DaemonConfig references environment variables for sensitive data. Never hardcode | Telegram | `TELEGRAM_BOT_TOKEN` | | Discord | `DISCORD_BOT_TOKEN`, `DISCORD_APPLICATION_ID` | | WhatsApp | `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_VERIFY_TOKEN` | +| Jira | `JIRA_USER_EMAIL`, `JIRA_API_TOKEN`, `JIRA_WEBHOOK_SECRET` (+ `JIRA_CLOUD_ID` or `base_url` in config) | **LLM API keys:** | Provider | Variable | @@ -478,6 +539,7 @@ The server exposes these endpoints for each platform: | GitHub | `https://your-domain/webhook/github` | | GitLab | `https://your-domain/webhook/gitlab` | | Bitbucket | `https://your-domain/webhook/bitbucket` | +| Jira | `https://your-domain/webhook/jira` | --- diff --git a/docs/reference/jira-integration.md b/docs/reference/jira-integration.md index a784eb1..869ce43 100644 --- a/docs/reference/jira-integration.md +++ b/docs/reference/jira-integration.md @@ -75,13 +75,25 @@ spec: platforms: jira: enabled: true - base_url: https://yourcompany.atlassian.net # Jira Cloud URL - auth: - type: api_token # api_token, oauth2, or pat - email_env: JIRA_EMAIL # For API token auth - token_env: JIRA_API_TOKEN + # Use base_url for direct Atlassian URL (recommended) + base_url: https://yourcompany.atlassian.net + # Or use cloud_id_env for Cloud ID based URL construction + # cloud_id_env: JIRA_CLOUD_ID + user_email_env: JIRA_USER_EMAIL + api_token_env: JIRA_API_TOKEN webhook_secret_env: JIRA_WEBHOOK_SECRET - bot_name: aofbot # Optional: for @mentions + bot_name: aofbot # Optional: name for comments + + # Optional: Restrict to specific projects + allowed_projects: + - PROJ + - DEV + + # Optional: Filter by event types + allowed_events: + - jira:issue_created + - jira:issue_updated + - comment_created # Resource discovery triggers: @@ -103,70 +115,43 @@ spec: task_timeout_secs: 300 ``` +**Webhook endpoint**: `https://your-domain.com/webhook/jira` + +> **Important**: When configuring Jira automation rules, use the full URL with `/webhook/jira` path, not just the base domain. + ### Platform Configuration Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `enabled` | bool | Yes | Enable Jira webhook endpoint (`/webhook/jira`) | -| `base_url` | string | Yes | Jira instance URL (Cloud or self-hosted) | -| `auth.type` | string | Yes | Authentication type: `api_token`, `oauth2`, or `pat` | -| `auth.email_env` | string | Conditional | Required for `api_token` auth | -| `auth.token_env` | string | Yes | Environment variable name for token/PAT | +| `base_url` | string | Yes* | Jira instance URL (e.g., `https://your-domain.atlassian.net`) | +| `cloud_id_env` | string | Yes* | Environment variable for Jira Cloud ID (alternative to base_url) | +| `user_email_env` | string | Yes | Environment variable name for user email | +| `api_token_env` | string | Yes | Environment variable name for API token | | `webhook_secret_env` | string | Yes | Environment variable name for webhook secret | -| `bot_name` | string | No | Bot name for @mentions (default: "aofbot") | - -#### Authentication Types - -**API Token (Recommended for Cloud):** -```yaml -auth: - type: api_token - email_env: JIRA_EMAIL - token_env: JIRA_API_TOKEN -``` - -**Personal Access Token (Server/Data Center):** -```yaml -auth: - type: pat - token_env: JIRA_PAT -``` +| `bot_name` | string | No | Bot name for comments (default: "aofbot") | +| `allowed_projects` | array | No | Project keys allowed to trigger (whitelist) | +| `allowed_events` | array | No | Event types to handle (whitelist) | -**OAuth 2.0 (Advanced):** -```yaml -auth: - type: oauth2 - token_env: JIRA_OAUTH_TOKEN - # Additional OAuth config... -``` +*Either `base_url` or `cloud_id_env` must be provided. ### Self-Hosted Jira Configuration -For Jira Server or Data Center deployments: +For Jira Server or Data Center deployments, use `base_url` pointing to your internal instance: ```yaml platforms: jira: enabled: true base_url: https://jira.yourcompany.com # Self-hosted URL - auth: - type: pat - token_env: JIRA_PAT + user_email_env: JIRA_USER_EMAIL + api_token_env: JIRA_API_TOKEN # Use PAT for Server/DC webhook_secret_env: JIRA_WEBHOOK_SECRET - - # Optional: Proxy configuration - proxy: - http_proxy: http://proxy.company.com:8080 - https_proxy: https://proxy.company.com:8080 - no_proxy: localhost,127.0.0.1 - - # Optional: TLS configuration - tls: - verify: true - ca_cert_path: /etc/ssl/certs/company-ca.pem ``` -> **Note**: Event filtering, project filtering, and command routing are configured in **Trigger** files, not in DaemonConfig. This separation keeps daemon config minimal and allows per-trigger customization. +> **Note**: For Jira Server/Data Center, create a Personal Access Token (PAT) instead of an API token. The configuration is the same - just store the PAT in `JIRA_API_TOKEN`. + +> **Note**: Event filtering, project filtering, and command routing can also be configured in **Trigger** files for per-trigger customization. ### Trigger Configuration @@ -1338,7 +1323,140 @@ spec: ## Webhook Setup -### 1. Create Webhook in Jira +There are two ways to configure Jira webhooks: + +### Option A: Jira Automation Rules (Project-Level) + +Use this method if you don't have Jira admin access or want per-project control. + +#### 1. Create Automation Rule + +1. Go to your Jira project +2. Navigate to **Project Settings** → **Automation** +3. Click **Create rule** +4. Choose a trigger (e.g., **When: Issue created**) +5. Add action → **Send web request** + +#### 2. Configure Web Request + +**URL**: `https://your-domain.com/webhook/jira` + +**HTTP method**: `POST` + +**Headers**: +| Key | Value | +|-----|-------| +| `Content-Type` | `application/json` | +| `X-Hub-Signature` | `` | + +**Web request body**: Select **Custom data** and use a payload template. + +#### 3. Payload Templates + +**Issue Created/Updated:** +```json +{ + "webhookEvent": "jira:issue_created", + "timestamp": {{now.asLong}}, + "issue": { + "id": "{{issue.id}}", + "key": "{{issue.key}}", + "fields": { + "summary": "{{issue.summary}}", + "description": "{{issue.description}}", + "issuetype": { "name": "{{issue.issueType.name}}" }, + "status": { "name": "{{issue.status.name}}" }, + "priority": { "name": "{{issue.priority.name}}" }, + "project": { + "key": "{{issue.project.key}}", + "name": "{{issue.project.name}}" + }, + "assignee": { + "displayName": "{{issue.assignee.displayName}}", + "accountId": "{{issue.assignee.accountId}}" + }, + "reporter": { + "displayName": "{{issue.reporter.displayName}}", + "accountId": "{{issue.reporter.accountId}}" + }, + "labels": {{issue.labels.asJsonArray}} + } + }, + "user": { + "accountId": "{{initiator.accountId}}", + "displayName": "{{initiator.displayName}}" + } +} +``` + +**Comment Created:** +```json +{ + "webhookEvent": "comment_created", + "timestamp": {{now.asLong}}, + "issue": { + "id": "{{issue.id}}", + "key": "{{issue.key}}", + "fields": { + "summary": "{{issue.summary}}", + "project": { + "key": "{{issue.project.key}}", + "name": "{{issue.project.name}}" + } + } + }, + "comment": { + "id": "{{comment.id}}", + "body": "{{comment.body}}", + "author": { + "accountId": "{{comment.author.accountId}}", + "displayName": "{{comment.author.displayName}}" + } + }, + "user": { + "accountId": "{{initiator.accountId}}", + "displayName": "{{initiator.displayName}}" + } +} +``` + +**Work Logged:** +```json +{ + "webhookEvent": "worklog_created", + "timestamp": {{now.asLong}}, + "issue": { + "id": "{{issue.id}}", + "key": "{{issue.key}}", + "fields": { + "summary": "{{issue.summary}}", + "issuetype": { "name": "{{issue.issueType.name}}" }, + "status": { "name": "{{issue.status.name}}" }, + "priority": { "name": "{{issue.priority.name}}" }, + "project": { + "key": "{{issue.project.key}}", + "name": "{{issue.project.name}}" + } + } + }, + "user": { + "accountId": "{{initiator.accountId}}", + "displayName": "{{initiator.displayName}}" + } +} +``` + +> **Important**: The `{{...}}` placeholders are Jira smart values. They get replaced with actual data when the webhook fires. + +#### 4. Signature Verification + +Jira Automation sends the `X-Hub-Signature` header value as a **static secret** (not computed HMAC). Your `JIRA_WEBHOOK_SECRET` environment variable must **exactly match** the value you configure in the header. + +--- + +### Option B: System Webhooks (Admin Only) + +Use this method if you have Jira admin access. System webhooks automatically include complete payloads. #### Jira Cloud @@ -1348,8 +1466,8 @@ spec: - **Name**: AOF Automation - **Status**: Enabled - **URL**: `https://your-domain.com/webhook/jira` - - **Secret**: Your `JIRA_WEBHOOK_SECRET` value - - **Events**: Select desired events or check "All issues" + - **Secret**: Your `JIRA_WEBHOOK_SECRET` value (enables HMAC verification) + - **Events**: Select desired events - **Exclude body**: Uncheck (AOF needs full payload) #### Jira Server/Data Center @@ -1358,7 +1476,9 @@ spec: 2. Create webhook with same configuration as Cloud 3. Ensure firewall allows webhook traffic to AOF daemon -### 2. Expose Endpoint +--- + +### Expose Endpoint **For production:** ```bash @@ -1380,19 +1500,19 @@ ngrok http 3000 Use tunnel URL as webhook URL in Jira. -### 3. Verify Webhook +### Verify Webhook -1. Test webhook in Jira webhook settings -2. Check webhook delivery logs in Jira -3. Verify AOF logs show received event +1. Test webhook using Jira's "Validate" button (Automation) or delivery logs (System webhooks) +2. Check AOF daemon logs for received events ```bash # Check logs -tail -f /var/log/aof/daemon.log +RUST_LOG=debug aofctl serve --config daemon.yaml # Look for: -# INFO Jira webhook received: issue_created -# INFO Posted comment to PROJ-123 +# INFO Received webhook for platform: jira +# DEBUG Jira signature verified via direct secret match +# INFO Processing event: jira:issue_created ``` --- diff --git a/docs/tutorials/jira-automation.md b/docs/tutorials/jira-automation.md index 0fd0cea..215631c 100644 --- a/docs/tutorials/jira-automation.md +++ b/docs/tutorials/jira-automation.md @@ -406,10 +406,19 @@ spec: platforms: jira: enabled: true - cloud_id_env: JIRA_CLOUD_ID + # Use base_url for direct Atlassian URL (recommended) + base_url: https://your-domain.atlassian.net + # Or use cloud_id_env for Cloud ID based URL construction + # cloud_id_env: JIRA_CLOUD_ID user_email_env: JIRA_USER_EMAIL api_token_env: JIRA_API_TOKEN webhook_secret_env: JIRA_WEBHOOK_SECRET + bot_name: aof-automation # Optional: name for comments + + # Optional: Restrict to specific projects + # allowed_projects: + # - PROJ + # - DEV # Resource directories triggers: @@ -425,7 +434,9 @@ spec: task_timeout_secs: 300 ``` -**Webhook endpoint**: `http://your-domain:3000/webhook/jira` +**Webhook endpoint**: `https://your-domain.com/webhook/jira` + +> **Important**: Configure your Jira automation rules to POST to `/webhook/jira`, not just the base URL. ## Step 9: Start the AOF Daemon @@ -458,25 +469,192 @@ Deploy to a server with HTTPS: # Webhook URL: https://aof.example.com/webhook/jira ``` -## Step 11: Configure Jira Webhook +## Step 11: Configure Jira Automation Webhook + +Jira Automation requires you to explicitly configure the webhook body. Here's how: + +### Creating the Automation Rule 1. Go to your Jira project 2. Navigate to **Project Settings** → **Automation** -3. Click **Create rule** → **When: Issue created** -4. Add action → **Send web request** -5. Configure: +3. Click **Create rule** +4. Choose a trigger (e.g., **When: Issue created**) +5. Add action → **Send web request** + +### Configuring the Web Request + +**URL**: +``` +https://your-domain.com/webhook/jira +``` + +**HTTP method**: `POST` + +**Headers** (click "Add another header"): + +| Key | Value | +|-----|-------| +| `Content-Type` | `application/json` | +| `X-Hub-Signature` | `` | + +**Web request body**: Select **Custom data** and paste the appropriate template below. + +### Payload Templates by Event Type + +#### Issue Created / Issue Updated + +```json +{ + "webhookEvent": "jira:issue_created", + "timestamp": {{now.asLong}}, + "issue": { + "id": "{{issue.id}}", + "key": "{{issue.key}}", + "fields": { + "summary": "{{issue.summary}}", + "description": "{{issue.description}}", + "issuetype": { + "name": "{{issue.issueType.name}}" + }, + "status": { + "name": "{{issue.status.name}}" + }, + "priority": { + "name": "{{issue.priority.name}}" + }, + "project": { + "key": "{{issue.project.key}}", + "name": "{{issue.project.name}}" + }, + "assignee": { + "displayName": "{{issue.assignee.displayName}}", + "accountId": "{{issue.assignee.accountId}}" + }, + "reporter": { + "displayName": "{{issue.reporter.displayName}}", + "accountId": "{{issue.reporter.accountId}}" + }, + "labels": {{issue.labels.asJsonArray}} + } + }, + "user": { + "accountId": "{{initiator.accountId}}", + "displayName": "{{initiator.displayName}}" + } +} +``` + +> **Note**: Change `"webhookEvent": "jira:issue_created"` to `"jira:issue_updated"` for update triggers. + +#### Comment Created + +```json +{ + "webhookEvent": "comment_created", + "timestamp": {{now.asLong}}, + "issue": { + "id": "{{issue.id}}", + "key": "{{issue.key}}", + "fields": { + "summary": "{{issue.summary}}", + "project": { + "key": "{{issue.project.key}}", + "name": "{{issue.project.name}}" + } + } + }, + "comment": { + "id": "{{comment.id}}", + "body": "{{comment.body}}", + "author": { + "accountId": "{{comment.author.accountId}}", + "displayName": "{{comment.author.displayName}}" + } + }, + "user": { + "accountId": "{{initiator.accountId}}", + "displayName": "{{initiator.displayName}}" + } +} +``` + +#### Work Logged + +```json +{ + "webhookEvent": "worklog_created", + "timestamp": {{now.asLong}}, + "issue": { + "id": "{{issue.id}}", + "key": "{{issue.key}}", + "fields": { + "summary": "{{issue.summary}}", + "description": "{{issue.description}}", + "issuetype": { + "name": "{{issue.issueType.name}}" + }, + "status": { + "name": "{{issue.status.name}}" + }, + "priority": { + "name": "{{issue.priority.name}}" + }, + "project": { + "key": "{{issue.project.key}}", + "name": "{{issue.project.name}}" + } + } + }, + "user": { + "accountId": "{{initiator.accountId}}", + "displayName": "{{initiator.displayName}}" + } +} +``` + +#### Sprint Started / Sprint Closed + +```json +{ + "webhookEvent": "sprint_started", + "timestamp": {{now.asLong}}, + "sprint": { + "id": {{sprint.id}}, + "name": "{{sprint.name}}", + "state": "{{sprint.state}}", + "goal": "{{sprint.goal}}" + }, + "user": { + "accountId": "{{initiator.accountId}}", + "displayName": "{{initiator.displayName}}" + } +} +``` + +### Important Notes + +1. **The `X-Hub-Signature` header value must exactly match your `JIRA_WEBHOOK_SECRET` environment variable** (case-sensitive) + +2. **Jira Automation sends a static secret**, not a computed HMAC signature. AOF supports both modes. + +3. **Smart values**: The `{{...}}` placeholders are Jira smart values that get replaced with actual data when the webhook fires. + +4. **Test your webhook**: After saving, use Jira's "Validate" button to test the configuration. + +### Alternative: System Webhooks (Admin Only) + +If you have Jira admin access, you can use built-in webhooks which automatically include full payloads: + +1. Go to **Settings** → **System** → **WebHooks** +2. Click **Create a WebHook** +3. Configure: + - **Name**: AOF Integration - **URL**: `https://your-domain.com/webhook/jira` - - **Headers**: Add `X-Hub-Signature` with webhook secret - - **HTTP method**: POST - - **Webhook body**: Issue data - - **Events**: Issue created, Issue updated -6. Click **Turn it on** - -**Alternative (Jira Cloud)**: -- Settings → System → Webhooks → Create Webhook -- URL: `https://your-domain.com/webhook/jira` -- Events: Issue created, updated, commented -- Secret: Your webhook secret + - **Secret**: Your webhook secret (for HMAC verification) + - **Events**: Select desired events +4. Click **Create** + +System webhooks automatically send complete payloads without manual body configuration. ## Step 12: Test Bug Triage From 2f395a9ac0ad7cea82448cc8c2698a8f0f840c0a Mon Sep 17 00:00:00 2001 From: Gopal Date: Sat, 3 Jan 2026 16:33:50 +0530 Subject: [PATCH 2/6] fix: Make Jira webhook payload fields optional for Automation compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make id fields optional in JiraIssueType, JiraProject, JiraStatus, JiraPriority, and JiraStatusCategory structs - Make self_url optional in JiraIssue and JiraComment structs - Make created field optional in JiraComment - Add debug logging for raw webhook payload - Support both direct secret match and HMAC signature verification These changes allow AOF to accept simpler webhook payloads from Jira Automation rules, which don't include all the fields that Jira's built-in system webhooks provide. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/aof-triggers/src/platforms/jira.rs | 40 ++++++++++++++++------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/crates/aof-triggers/src/platforms/jira.rs b/crates/aof-triggers/src/platforms/jira.rs index e816c59..320dd26 100644 --- a/crates/aof-triggers/src/platforms/jira.rs +++ b/crates/aof-triggers/src/platforms/jira.rs @@ -229,7 +229,8 @@ pub struct JiraIssueFields { /// Jira issue type #[derive(Debug, Clone, Deserialize)] pub struct JiraIssueType { - pub id: String, + #[serde(default)] + pub id: Option, pub name: String, #[serde(default)] pub description: Option, @@ -238,7 +239,8 @@ pub struct JiraIssueType { /// Jira project information #[derive(Debug, Clone, Deserialize)] pub struct JiraProject { - pub id: String, + #[serde(default)] + pub id: Option, pub key: String, pub name: String, } @@ -247,7 +249,8 @@ pub struct JiraProject { #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct JiraStatus { - pub id: String, + #[serde(default)] + pub id: Option, pub name: String, #[serde(default)] pub status_category: Option, @@ -257,15 +260,18 @@ pub struct JiraStatus { #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct JiraStatusCategory { - pub id: i64, - pub key: String, + #[serde(default)] + pub id: Option, + #[serde(default)] + pub key: Option, pub name: String, } /// Jira priority #[derive(Debug, Clone, Deserialize)] pub struct JiraPriority { - pub id: String, + #[serde(default)] + pub id: Option, pub name: String, } @@ -274,8 +280,8 @@ pub struct JiraPriority { pub struct JiraIssue { pub id: String, pub key: String, - #[serde(rename = "self")] - pub self_url: String, + #[serde(rename = "self", default)] + pub self_url: Option, pub fields: JiraIssueFields, } @@ -283,15 +289,17 @@ pub struct JiraIssue { #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct JiraComment { - pub id: String, - #[serde(rename = "self")] - pub self_url: String, + #[serde(default)] + pub id: Option, + #[serde(rename = "self", default)] + pub self_url: Option, pub body: String, #[serde(default)] pub author: Option, #[serde(default)] pub update_author: Option, - pub created: String, + #[serde(default)] + pub created: Option, #[serde(default)] pub updated: Option, } @@ -884,6 +892,14 @@ impl TriggerPlatform for JiraPlatform { raw: &[u8], headers: &HashMap, ) -> Result { + // Log raw payload for debugging + if let Ok(raw_str) = std::str::from_utf8(raw) { + debug!("Jira webhook raw payload ({} bytes): {}", raw.len(), + if raw_str.len() > 500 { &raw_str[..500] } else { raw_str }); + } else { + debug!("Jira webhook raw payload ({} bytes): ", raw.len()); + } + // Verify signature if present if let Some(signature) = headers.get("x-hub-signature") { if !self.verify_jira_signature(raw, signature) { From a7f7fe47086c2ef8bd3f859e1ddfe80cff181aeb Mon Sep 17 00:00:00 2001 From: Gopal Date: Sat, 3 Jan 2026 16:42:14 +0530 Subject: [PATCH 3/6] docs: Update Jira webhook payload templates with minimal examples and curl testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add minimal payload templates for quick testing - Add full payload templates with all fields for production use - Add curl testing section for debugging webhooks without Jira - Simplify field requirements documentation - Document signature header for Jira Automation rules 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docs/reference/jira-integration.md | 83 ++++++++---------- docs/tutorials/jira-automation.md | 136 +++++++++++++++-------------- 2 files changed, 108 insertions(+), 111 deletions(-) diff --git a/docs/reference/jira-integration.md b/docs/reference/jira-integration.md index 869ce43..0fcdc01 100644 --- a/docs/reference/jira-integration.md +++ b/docs/reference/jira-integration.md @@ -1353,39 +1353,24 @@ Use this method if you don't have Jira admin access or want per-project control. #### 3. Payload Templates -**Issue Created/Updated:** +AOF accepts flexible payloads - most fields are optional. Use minimal templates or add more fields as needed. + +**Issue Created/Updated (Minimal):** ```json { "webhookEvent": "jira:issue_created", - "timestamp": {{now.asLong}}, + "timestamp": {{now.epochMillis}}, "issue": { "id": "{{issue.id}}", "key": "{{issue.key}}", "fields": { "summary": "{{issue.summary}}", - "description": "{{issue.description}}", "issuetype": { "name": "{{issue.issueType.name}}" }, "status": { "name": "{{issue.status.name}}" }, - "priority": { "name": "{{issue.priority.name}}" }, - "project": { - "key": "{{issue.project.key}}", - "name": "{{issue.project.name}}" - }, - "assignee": { - "displayName": "{{issue.assignee.displayName}}", - "accountId": "{{issue.assignee.accountId}}" - }, - "reporter": { - "displayName": "{{issue.reporter.displayName}}", - "accountId": "{{issue.reporter.accountId}}" - }, - "labels": {{issue.labels.asJsonArray}} + "project": { "key": "{{issue.project.key}}", "name": "{{issue.project.name}}" } } }, - "user": { - "accountId": "{{initiator.accountId}}", - "displayName": "{{initiator.displayName}}" - } + "user": { "accountId": "{{initiator.accountId}}", "displayName": "{{initiator.displayName}}" } } ``` @@ -1393,30 +1378,20 @@ Use this method if you don't have Jira admin access or want per-project control. ```json { "webhookEvent": "comment_created", - "timestamp": {{now.asLong}}, + "timestamp": {{now.epochMillis}}, "issue": { "id": "{{issue.id}}", "key": "{{issue.key}}", "fields": { "summary": "{{issue.summary}}", - "project": { - "key": "{{issue.project.key}}", - "name": "{{issue.project.name}}" - } + "project": { "key": "{{issue.project.key}}", "name": "{{issue.project.name}}" } } }, "comment": { - "id": "{{comment.id}}", "body": "{{comment.body}}", - "author": { - "accountId": "{{comment.author.accountId}}", - "displayName": "{{comment.author.displayName}}" - } + "author": { "accountId": "{{comment.author.accountId}}", "displayName": "{{comment.author.displayName}}" } }, - "user": { - "accountId": "{{initiator.accountId}}", - "displayName": "{{initiator.displayName}}" - } + "user": { "accountId": "{{initiator.accountId}}", "displayName": "{{initiator.displayName}}" } } ``` @@ -1424,7 +1399,7 @@ Use this method if you don't have Jira admin access or want per-project control. ```json { "webhookEvent": "worklog_created", - "timestamp": {{now.asLong}}, + "timestamp": {{now.epochMillis}}, "issue": { "id": "{{issue.id}}", "key": "{{issue.key}}", @@ -1432,22 +1407,40 @@ Use this method if you don't have Jira admin access or want per-project control. "summary": "{{issue.summary}}", "issuetype": { "name": "{{issue.issueType.name}}" }, "status": { "name": "{{issue.status.name}}" }, - "priority": { "name": "{{issue.priority.name}}" }, - "project": { - "key": "{{issue.project.key}}", - "name": "{{issue.project.name}}" - } + "project": { "key": "{{issue.project.key}}", "name": "{{issue.project.name}}" } } }, - "user": { - "accountId": "{{initiator.accountId}}", - "displayName": "{{initiator.displayName}}" - } + "user": { "accountId": "{{initiator.accountId}}", "displayName": "{{initiator.displayName}}" } } ``` > **Important**: The `{{...}}` placeholders are Jira smart values. They get replaced with actual data when the webhook fires. +#### Testing with curl + +Test the endpoint before configuring Jira: + +```bash +curl -X POST https://your-domain.com/webhook/jira \ + -H "Content-Type: application/json" \ + -H "X-Hub-Signature: YOUR_SECRET_HERE" \ + -d '{ + "webhookEvent": "jira:issue_created", + "timestamp": 1735897519000, + "issue": { + "id": "10005", + "key": "PROJ-123", + "fields": { + "summary": "Test issue", + "issuetype": { "name": "Bug" }, + "status": { "name": "To Do" }, + "project": { "key": "PROJ", "name": "My Project" } + } + }, + "user": { "accountId": "test", "displayName": "Test User" } + }' +``` + #### 4. Signature Verification Jira Automation sends the `X-Hub-Signature` header value as a **static secret** (not computed HMAC). Your `JIRA_WEBHOOK_SECRET` environment variable must **exactly match** the value you configure in the header. diff --git a/docs/tutorials/jira-automation.md b/docs/tutorials/jira-automation.md index 215631c..eeca0d8 100644 --- a/docs/tutorials/jira-automation.md +++ b/docs/tutorials/jira-automation.md @@ -501,46 +501,49 @@ https://your-domain.com/webhook/jira ### Payload Templates by Event Type -#### Issue Created / Issue Updated +AOF accepts flexible payloads - most fields are optional. Use the minimal templates below, or add more fields as needed. + +#### Issue Created / Issue Updated (Minimal) + +```json +{ + "webhookEvent": "jira:issue_created", + "timestamp": {{now.epochMillis}}, + "issue": { + "id": "{{issue.id}}", + "key": "{{issue.key}}", + "fields": { + "summary": "{{issue.summary}}", + "issuetype": { "name": "{{issue.issueType.name}}" }, + "status": { "name": "{{issue.status.name}}" }, + "project": { "key": "{{issue.project.key}}", "name": "{{issue.project.name}}" } + } + }, + "user": { "accountId": "{{initiator.accountId}}", "displayName": "{{initiator.displayName}}" } +} +``` + +#### Issue Created / Issue Updated (Full) ```json { "webhookEvent": "jira:issue_created", - "timestamp": {{now.asLong}}, + "timestamp": {{now.epochMillis}}, "issue": { "id": "{{issue.id}}", "key": "{{issue.key}}", "fields": { "summary": "{{issue.summary}}", "description": "{{issue.description}}", - "issuetype": { - "name": "{{issue.issueType.name}}" - }, - "status": { - "name": "{{issue.status.name}}" - }, - "priority": { - "name": "{{issue.priority.name}}" - }, - "project": { - "key": "{{issue.project.key}}", - "name": "{{issue.project.name}}" - }, - "assignee": { - "displayName": "{{issue.assignee.displayName}}", - "accountId": "{{issue.assignee.accountId}}" - }, - "reporter": { - "displayName": "{{issue.reporter.displayName}}", - "accountId": "{{issue.reporter.accountId}}" - }, - "labels": {{issue.labels.asJsonArray}} + "issuetype": { "name": "{{issue.issueType.name}}" }, + "status": { "name": "{{issue.status.name}}" }, + "priority": { "name": "{{issue.priority.name}}" }, + "project": { "key": "{{issue.project.key}}", "name": "{{issue.project.name}}" }, + "assignee": { "displayName": "{{issue.assignee.displayName}}", "accountId": "{{issue.assignee.accountId}}" }, + "reporter": { "displayName": "{{issue.reporter.displayName}}", "accountId": "{{issue.reporter.accountId}}" } } }, - "user": { - "accountId": "{{initiator.accountId}}", - "displayName": "{{initiator.displayName}}" - } + "user": { "accountId": "{{initiator.accountId}}", "displayName": "{{initiator.displayName}}" } } ``` @@ -551,30 +554,20 @@ https://your-domain.com/webhook/jira ```json { "webhookEvent": "comment_created", - "timestamp": {{now.asLong}}, + "timestamp": {{now.epochMillis}}, "issue": { "id": "{{issue.id}}", "key": "{{issue.key}}", "fields": { "summary": "{{issue.summary}}", - "project": { - "key": "{{issue.project.key}}", - "name": "{{issue.project.name}}" - } + "project": { "key": "{{issue.project.key}}", "name": "{{issue.project.name}}" } } }, "comment": { - "id": "{{comment.id}}", "body": "{{comment.body}}", - "author": { - "accountId": "{{comment.author.accountId}}", - "displayName": "{{comment.author.displayName}}" - } + "author": { "accountId": "{{comment.author.accountId}}", "displayName": "{{comment.author.displayName}}" } }, - "user": { - "accountId": "{{initiator.accountId}}", - "displayName": "{{initiator.displayName}}" - } + "user": { "accountId": "{{initiator.accountId}}", "displayName": "{{initiator.displayName}}" } } ``` @@ -583,32 +576,19 @@ https://your-domain.com/webhook/jira ```json { "webhookEvent": "worklog_created", - "timestamp": {{now.asLong}}, + "timestamp": {{now.epochMillis}}, "issue": { "id": "{{issue.id}}", "key": "{{issue.key}}", "fields": { "summary": "{{issue.summary}}", - "description": "{{issue.description}}", - "issuetype": { - "name": "{{issue.issueType.name}}" - }, - "status": { - "name": "{{issue.status.name}}" - }, - "priority": { - "name": "{{issue.priority.name}}" - }, - "project": { - "key": "{{issue.project.key}}", - "name": "{{issue.project.name}}" - } + "issuetype": { "name": "{{issue.issueType.name}}" }, + "status": { "name": "{{issue.status.name}}" }, + "priority": { "name": "{{issue.priority.name}}" }, + "project": { "key": "{{issue.project.key}}", "name": "{{issue.project.name}}" } } }, - "user": { - "accountId": "{{initiator.accountId}}", - "displayName": "{{initiator.displayName}}" - } + "user": { "accountId": "{{initiator.accountId}}", "displayName": "{{initiator.displayName}}" } } ``` @@ -617,20 +597,44 @@ https://your-domain.com/webhook/jira ```json { "webhookEvent": "sprint_started", - "timestamp": {{now.asLong}}, + "timestamp": {{now.epochMillis}}, "sprint": { "id": {{sprint.id}}, "name": "{{sprint.name}}", "state": "{{sprint.state}}", "goal": "{{sprint.goal}}" }, - "user": { - "accountId": "{{initiator.accountId}}", - "displayName": "{{initiator.displayName}}" - } + "user": { "accountId": "{{initiator.accountId}}", "displayName": "{{initiator.displayName}}" } } ``` +### Testing with curl + +Before configuring Jira, test the endpoint directly: + +```bash +curl -X POST https://your-ngrok-url.ngrok-free.dev/webhook/jira \ + -H "Content-Type: application/json" \ + -H "X-Hub-Signature: YOUR_SECRET_HERE" \ + -d '{ + "webhookEvent": "worklog_created", + "timestamp": 1735897519000, + "issue": { + "id": "10005", + "key": "SCRUM-5", + "fields": { + "summary": "Test issue", + "issuetype": { "name": "Task" }, + "status": { "name": "To Do" }, + "project": { "key": "SCRUM", "name": "Team Astro" } + } + }, + "user": { "accountId": "test", "displayName": "Test User" } + }' +``` + +Replace `YOUR_SECRET_HERE` with your `JIRA_WEBHOOK_SECRET` value. + ### Important Notes 1. **The `X-Hub-Signature` header value must exactly match your `JIRA_WEBHOOK_SECRET` environment variable** (case-sensitive) From a99154ad6d9f4b6b08befee9824d11afc917025f Mon Sep 17 00:00:00 2001 From: Gopal Date: Sat, 3 Jan 2026 17:31:29 +0530 Subject: [PATCH 4/6] fix: Make Jira webhook timestamp field optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jira Automation doesn't substitute smart values during validation when no work item key is provided, resulting in empty timestamp values. Made timestamp optional and default to current time when not provided. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/aof-triggers/src/platforms/jira.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/aof-triggers/src/platforms/jira.rs b/crates/aof-triggers/src/platforms/jira.rs index 320dd26..0607e34 100644 --- a/crates/aof-triggers/src/platforms/jira.rs +++ b/crates/aof-triggers/src/platforms/jira.rs @@ -347,8 +347,9 @@ pub struct JiraChangelog { #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct JiraWebhookPayload { - /// Webhook event timestamp - pub timestamp: i64, + /// Webhook event timestamp (optional - may not be provided by Jira Automation) + #[serde(default)] + pub timestamp: Option, /// Event type pub webhook_event: String, @@ -861,8 +862,9 @@ impl JiraPlatform { metadata.insert("changelog".to_string(), serde_json::to_value(changelog).unwrap_or_default()); } - // Message ID from issue and timestamp - let message_id = format!("jira-{}-{}-{}", issue.id, event_type, payload.timestamp); + // Message ID from issue and timestamp (use current time if not provided) + let ts = payload.timestamp.unwrap_or_else(|| chrono::Utc::now().timestamp_millis()); + let message_id = format!("jira-{}-{}-{}", issue.id, event_type, ts); // Thread ID from issue key let thread_id = Some(issue.key.clone()); @@ -873,7 +875,7 @@ impl JiraPlatform { channel_id, user: trigger_user, text, - timestamp: chrono::DateTime::from_timestamp(payload.timestamp / 1000, 0).unwrap_or_else(chrono::Utc::now), + timestamp: chrono::DateTime::from_timestamp(ts / 1000, 0).unwrap_or_else(chrono::Utc::now), metadata, thread_id, reply_to: None, From 34181e658e7d42989d37e2508a9036dc4448b544 Mon Sep 17 00:00:00 2001 From: Gopal Date: Sat, 3 Jan 2026 18:36:27 +0530 Subject: [PATCH 5/6] fix: Make JiraProject.name field optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jira Automation smart values may not populate all fields during webhook validation. Made project name optional to handle minimal payloads. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/aof-triggers/src/platforms/jira.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/aof-triggers/src/platforms/jira.rs b/crates/aof-triggers/src/platforms/jira.rs index 0607e34..cdc9fd0 100644 --- a/crates/aof-triggers/src/platforms/jira.rs +++ b/crates/aof-triggers/src/platforms/jira.rs @@ -242,7 +242,8 @@ pub struct JiraProject { #[serde(default)] pub id: Option, pub key: String, - pub name: String, + #[serde(default)] + pub name: Option, } /// Jira status From e0a8b4065b83f6e1135e3c131dc6e50f305c2f5b Mon Sep 17 00:00:00 2001 From: Gopal Date: Sat, 3 Jan 2026 18:40:38 +0530 Subject: [PATCH 6/6] fix: Make JiraIssue.id field optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use issue key as fallback when id is not provided in payload. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/aof-triggers/src/platforms/jira.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/aof-triggers/src/platforms/jira.rs b/crates/aof-triggers/src/platforms/jira.rs index cdc9fd0..c53ef6e 100644 --- a/crates/aof-triggers/src/platforms/jira.rs +++ b/crates/aof-triggers/src/platforms/jira.rs @@ -279,7 +279,8 @@ pub struct JiraPriority { /// Jira issue information #[derive(Debug, Clone, Deserialize)] pub struct JiraIssue { - pub id: String, + #[serde(default)] + pub id: Option, pub key: String, #[serde(rename = "self", default)] pub self_url: Option, @@ -837,7 +838,9 @@ impl JiraPlatform { // Build metadata with full event details let mut metadata = HashMap::new(); metadata.insert("event_type".to_string(), serde_json::json!(event_type)); - metadata.insert("issue_id".to_string(), serde_json::json!(issue.id)); + if let Some(ref id) = issue.id { + metadata.insert("issue_id".to_string(), serde_json::json!(id)); + } metadata.insert("issue_key".to_string(), serde_json::json!(issue.key)); metadata.insert("issue_type".to_string(), serde_json::json!(issue.fields.issuetype.name)); metadata.insert("project_id".to_string(), serde_json::json!(issue.fields.project.id)); @@ -865,7 +868,8 @@ impl JiraPlatform { // Message ID from issue and timestamp (use current time if not provided) let ts = payload.timestamp.unwrap_or_else(|| chrono::Utc::now().timestamp_millis()); - let message_id = format!("jira-{}-{}-{}", issue.id, event_type, ts); + let issue_id = issue.id.as_deref().unwrap_or(&issue.key); + let message_id = format!("jira-{}-{}-{}", issue_id, event_type, ts); // Thread ID from issue key let thread_id = Some(issue.key.clone());