# Jira Event Forwarder

LLMS index: [llms.txt](https://hummingbird-project.io/llms.txt) | Full content: [llms-full.txt](https://hummingbird-project.io/llms-full.txt)

---

An AWS Lambda function that receives [Jira Automation][jira-automation] webhook
events and forwards them to an SNS topic with structured metadata for filtering.
Validates Bearer token authentication and restricts access to [Atlassian egress
IP ranges][atlassian-ips] via an API Gateway resource policy.

The original Jira JSON payload is forwarded unchanged as the SNS message body.

## Features

- **Bearer Token Authentication**: Validates `Authorization: Bearer <token>`
  headers with constant-time comparison, supporting multiple tokens for
  zero-downtime rotation
- **Atlassian IP Restriction**: API Gateway resource policy restricts access to
  Atlassian egress IP ranges (IPv4 and IPv6), fetched dynamically at deploy time
- **Structured Metadata**: Extracts event type, project key, and issue type as
  SNS message attributes
- **SNS Subscription Filtering**: Enables precise event routing to subscribers
- **Custom Domain**: Optional custom domain with automatic TLS certificate
  management via ACM and Route53

## Architecture

Single Lambda function handles the webhook processing:

1. **Handler** - API Gateway endpoint (`/webhook`) that validates the Bearer
   token, extracts metadata from the Jira payload and request headers, and
   publishes to SNS

## Prerequisites

- AWS CLI configured with appropriate credentials (IAM permissions for Lambda,
  API Gateway, SNS, CloudFormation, CloudWatch Logs, and optionally Route53/ACM
  for custom domain)
- Podman or Docker (for containerized SAM build/deploy)
- Python 3.11 or later (for development)

## Deployment

Build and deploy using containerized AWS SAM CLI:

```bash
cd jira-event-forwarder
make build     # Build Lambda package
make deploy    # First deployment (interactive/guided)
make redeploy  # Subsequent deployments (non-interactive)
```

**Deployment outputs:** `ApiEndpoint` - API Gateway endpoint URL
(`<api-endpoint>`)

### Custom Domain

Optional custom domain with automatic TLS certificate management (ACM +
Route53). Requires a Route53 hosted zone. Deploy with `CustomDomainName` and
`HostedZoneId` parameters - CloudFormation handles certificate creation, DNS
validation, and configuration. Certificate validation takes 5-30 minutes; allow
up to 1 hour for DNS propagation.

### Parameters

| Parameter            | Description                              | Default      |
| -------------------- | ---------------------------------------- | ------------ |
| `ResourcePrefix`     | Prefix for resources                     | `myapp-prod` |
| `SnsTopicArn`        | SNS topic ARN                            | (required)   |
| `JiraWebhookTokens`  | Jira webhook tokens (JSON array)         | `[]`         |
| `SentryDsn`          | Optional Sentry DSN                      | ``           |
| `AtlassianIpv4Cidrs` | Atlassian egress IPv4 CIDRs (comma-list) | ``           |
| `AtlassianIpv6Cidrs` | Atlassian egress IPv6 CIDRs (comma-list) | ``           |
| `CustomDomainName`   | Custom domain name                       | ``           |
| `HostedZoneId`       | Route53 hosted zone                      | ``           |

**Resource naming:** Lambda and API resources follow
`{ResourcePrefix}-{type}-{name}` pattern (e.g., `myapp-prod-lambda-handler`,
`myapp-prod-api`).

**Prerequisites:** Requires an existing SNS topic. Deploy
[hummingbird-events-topic][het-docs] first to create the topic, then use its
ARN for the `SnsTopicArn` parameter.

[het-docs]: https://gitlab.com/redhat/hummingbird/tools/-/blob/main/documentation/hummingbird-events-topic.md

## Usage

Jira Automation allows only one trigger per rule, and the "Multiple issue
events" trigger does not expose which event fired. Since each event needs a
distinct `X-Jira-Event-Type` header, configure 6 separate automation rules
— one per event type:

| Trigger      | `X-Jira-Event-Type` value |
| ------------ | ------------------------- |
| Created      | `issue_created`           |
| Updated      | `issue_updated`           |
| Deleted      | `issue_deleted`           |
| Assigned     | `issue_assigned`          |
| Transitioned | `issue_transitioned`      |
| Moved        | `issue_moved`             |

Each rule's "Send web request" action:

- **URL**: `<api-endpoint>`
- **Method**: POST
- **Headers**:
  - `Authorization: Bearer <token>` — enter the token value and click
    "Hidden" to mask it in the Jira Automation UI
  - `X-Jira-Event-Type: <value from table above>`
- **Body**: "Issue data (Jira format)"

**SNS Subscription Filter Examples:**

All Jira issue events:

```json
{
  "source": ["jira"]
}
```

Specific Jira event types:

```json
{
  "source": ["jira"],
  "event_type": ["issue_created", "issue_updated"]
}
```

Events from a specific project:

```json
{
  "source": ["jira"],
  "project_key": ["HUM"]
}
```

## Development

See the main [README][readme] for development workflows.

```bash
make setup     # Install dependencies
make check     # Lint code (ruff)
make fmt       # Format code
make test      # Run unit tests
make coverage  # Run tests with coverage
```

## Configuration

Lambda function receives configuration via environment variables (automatically
set by CloudFormation):

| Variable              | Description                      |
| --------------------- | -------------------------------- |
| `SNS_TOPIC_ARN`       | SNS topic ARN                    |
| `JIRA_WEBHOOK_TOKENS` | Jira webhook tokens (JSON array) |
| `SENTRY_DSN`          | Optional Sentry DSN              |
| `AWS_REGION`          | AWS region (auto-set)            |

## Event Metadata

The Lambda function extracts metadata from Jira webhook events and request
headers, adding them as SNS message attributes:

| Attribute          | Description                     | Example         |
| ------------------ | ------------------------------- | --------------- |
| `source`           | Always `"jira"`                 | `jira`          |
| `event_type`       | From `X-Jira-Event-Type` header | `issue_created` |
| `project_key`      | Jira project key                | `HUM`           |
| `issue_type`       | Issue type name                 | `Story`, `Bug`  |
| `content_encoding` | Compression format              | `gzip+base64`   |

The `event_type` attribute is set from the `X-Jira-Event-Type` request header.
Each Jira Automation rule hardcodes the appropriate value for its trigger type
in this header (see [Usage](#usage)). If the header is absent, the `event_type`
attribute is omitted from the SNS message.

## Security & Limitations

**Security:**

- Bearer token validation via `Authorization` header with constant-time
  comparison (`hmac.compare_digest`)
- API Gateway resource policy restricting source IPs to Atlassian egress
  ranges (IPv4 and IPv6 CIDRs both via `aws:SourceIp`), fetched dynamically
  at deploy time from `ip-ranges.atlassian.com`
- Supports multiple active tokens for zero-downtime rotation
- Missing or invalid tokens return HTTP 401; no configured tokens returns
  HTTP 500
- SNS topic follows least privilege principle
- CloudWatch logs capture all webhook deliveries (7-day retention)
- Sentry integration for error tracking

**Limitations:**

- Lambda timeout: 30 seconds
- Lambda memory: 256 MB
- Webhook payload size: Up to 6 MB (API Gateway limit)
- SNS limits the compressed and base64-encoded message plus attributes to
  256 KiB

The forwarder compresses the original JSON for SNS transport. Consumers that
decode `gzip+base64` receive the unchanged webhook payload.

## License

This project is licensed under the GNU General Public License v3.0 or later -
see the [LICENSE][license] file for details.

[jira-automation]: https://support.atlassian.com/cloud-automation/docs/jira-cloud-automation/
[atlassian-ips]: https://ip-ranges.atlassian.com/
[readme]: https://gitlab.com/redhat/hummingbird/tools/-/blob/main/README.md
[license]: https://gitlab.com/redhat/hummingbird/tools/-/blob/main/LICENSE
