# jira_cli

AWS Lambda + CLI tool that queries Jira for open offboarding tickets, extracts structured employee data (email, full name, last working day) from ticket descriptions, and writes feedback back to the ticket (comment, due date, label). Part of the employee offboarding automation Step Functions workflow.

## Features

- Query all open offboarding Jira tickets via JQL
- Query overdue offboarding tickets (due date has passed)
- Parse Atlassian Document Format (ADF) ticket descriptions to extract email, full name, and last working day
- Post a plain-text comment on a ticket (e.g. automation result summary)
- Set the ticket due date to the employee's last working day
- Apply a label to a ticket to mark its terminal state and prevent re-processing
- Retry on 429/5xx responses with exponential backoff
- Runs as an AWS Lambda handler or as a local CLI tool

## Requirements

- Python 3.14+
- [uv](https://docs.astral.sh/uv/)

## Setup

```bash
cd lambda/jira_cli
uv sync --group dev
```

## Environment variables

| Variable | Description | Example |
|---|---|---|
| `JIRA_BASE_URL` | Base URL of your Jira instance | `https://yourorg.atlassian.net` |
| `JIRA_USER_EMAIL` | Email address used for Jira Basic Auth | `bot@example.com` |
| `JIRA_API_TOKEN` | Jira API token (not your password) | `ATATT3...` |

## CLI usage

```bash
uv run jira-cli query-offboarding-tickets
```

Output is a JSON object printed to stdout:

```json
{
  "tickets": [
    {
      "id": "SYS-1234",
      "email": "user@example.com",
      "full_name": "Jane Doe",
      "last_working_day": "2026-04-01"
    }
  ]
}
```

## Lambda handler

`jira_client/app.py` is the Lambda entry point. It routes on `event["action"]`:

### `query-offboarding-tickets`

Fetches all newly created, unassigned offboarding tickets (created in the last 14 days) and returns parsed employee data. Tickets with a missing description, email, full name, or last working day are skipped with a warning log.

Request:

```json
{ "action": "query-offboarding-tickets" }
```

Response:

```json
{
  "tickets": [
    {
      "id": "SYS-1234",
      "email": "user@example.com",
      "full_name": "Jane Doe",
      "last_working_day": "2026-04-01"
    }
  ]
}
```

### `query-completed-tickets`

Fetches SYS tickets that have reached a terminal automation state and are not yet in the `Closed` status — those labelled `automation-complete` / `suspension-complete` (work was performed) or `no-actions-required` / `suspend-no-actions-required` (nothing to do). Used by the close state machine to close tickets on a later iteration. Unlike the other query actions, this one does **not** parse descriptions: closing needs only the issue key, so tickets whose descriptions no longer parse are still returned (they must still be closeable).

Request:

```json
{ "action": "query-completed-tickets" }
```

Response (key only):

```json
{
  "tickets": [
    { "id": "SYS-1234" }
  ]
}
```

### `query-overdue-tickets`

Fetches all open offboarding tickets whose due date has passed (`duedate <= now()`). Used in a second Step Functions pass after `add-due-date` has stamped `last_working_day` onto each ticket. Returns the same structure as `query-offboarding-tickets`.

Request:

```json
{ "action": "query-overdue-tickets" }
```

Response:

```json
{
  "tickets": [
    {
      "id": "SYS-1234",
      "email": "user@example.com",
      "full_name": "Jane Doe",
      "last_working_day": "2026-04-01"
    }
  ]
}
```

### `add-comment`

Posts a plain-text comment on a Jira ticket. The text is automatically wrapped in an Atlassian Document Format (ADF) document as required by Jira REST API v3. Supports `dry_run`.

Request:

```json
{
  "action": "add-comment",
  "ticket_id": "SYS-1234",
  "comment": "Automation complete. Auth0 user deleted from 3 tenants.",
  "dry_run": false
}
```

Response:

```json
{ "ticket_id": "SYS-1234", "dry_run": false, "commented": true }
```

### `add-due-date`

Sets the due date field on a Jira ticket. Typically used to stamp `last_working_day` onto the ticket so future-dated offboardings are easy to identify in Jira's date-based views, and so the `query-overdue-tickets` JQL filter can later select them. Supports `dry_run`.

Request:

```json
{
  "action": "add-due-date",
  "ticket_id": "SYS-1234",
  "due_date": "2026-04-01",
  "dry_run": false
}
```

Response:

```json
{ "ticket_id": "SYS-1234", "dry_run": false, "due_date_set": true }
```

### `add-label`

Appends a label to a Jira ticket without overwriting existing labels (uses the Jira `update` operation). Supports `dry_run`.

Applying **any** label causes the ticket to be excluded from future automation runs because the JQL query filters `AND labels = empty`.

Request:

```json
{
  "action": "add-label",
  "ticket_id": "SYS-1234",
  "label": "automation-complete",
  "dry_run": false
}
```

Response:

```json
{ "ticket_id": "SYS-1234", "dry_run": false, "label_added": true }
```

#### Available labels (`JiraLabels` in `src/constants.py`)

| Label | When to use |
|---|---|
| `automation-complete` | All offboarding steps (Auth0 + GitHub) ran successfully |
| `no-actions-required` | User was not found in any system — nothing to clean up |

## Docker

Start the Lambda locally and invoke it with curl:

```bash
docker compose up --build function

curl -X POST http://localhost:9000/2015-03-31/functions/function/invocations \
  -H 'Content-Type: application/json' \
  -d '{"action": "query-offboarding-tickets"}'
```

Run unit tests and lint inside Docker (mirrors CI):

```bash
make docker_test
make docker_test_integration
```

## Testing

```bash
uv run pytest ./tests/                                                          # All tests
uv run pytest ./tests/unit/                                                     # Unit tests only
uv run pytest ./tests/integration/                                              # Integration tests only
uv run pytest ./tests/unit/test_jira_client.py                                  # Single file
uv run pytest ./tests/unit/test_jira_client.py::test_query_jira_tickets_success # Single test
```

## Lint & format

```bash
uv run ruff check src/ tests/
uv run ruff format src/ tests/
uv run mypy src/
```

## Architecture

### Step Functions flow

The typical offboarding flow for this Lambda is:

1. **`query-offboarding-tickets`** — fetch newly created backlog tickets, parse employee data
2. **`add-due-date`** (per ticket) — stamp `last_working_day` as the Jira due date
3. *(Wait until due date)*
4. **`query-overdue-tickets`** — fetch tickets whose due date has passed, trigger downstream Auth0 + GitHub cleanup

### JQL queries (`src/constants.py`)

| Name | Description |
|---|---|
| `ALL_OFFBOARDING_TICKETS` | All open offboarding tickets with no labels |
| `MY_OFFBOARDING_TICKETS` | Tickets assigned to current user, status "In Development" |
| `NEW_OPEN_OFFBOARDING_TICKETS` | Unassigned tickets created in the last 14 days (used by `query-offboarding-tickets`) |
| `OVERDUE_OFFBOARDING_TICKETS` | Open tickets whose due date has passed (used by `query-overdue-tickets`) |

### Label constants (`src/constants.py`)

`JiraLabels` defines the approved label strings for marking ticket terminal states. Any label applied via `add-label` removes the ticket from future automation runs because the JQL filters `AND labels = empty`.

### Description parsing (`src/utils.py`)

Ticket descriptions are returned as Atlassian Document Format (ADF) JSON. The tool looks for paragraphs containing `"An offboarding request has been submitted"` and extracts:

- **Email** — via `RegexPatterns.EMAIL_RE`
- **Full name** — from `"request has been submitted for {name}."` pattern
- **Last working day** — from `"Last day of Employment: YYYY-MM-DD"`

Text is normalized with `TextUtils.clean_text()` before parsing.

### Authentication (`src/jira_client.py`)

Uses HTTP Basic Auth with `JIRA_USER_EMAIL` and `JIRA_API_TOKEN`. 401 and 403 responses raise descriptive errors before the generic `raise_for_status()`. The session retries on 429/5xx with exponential backoff.

## Project structure

```
lambda/jira_cli/
├── config.py               # JiraConfig (pydantic-settings, reads env vars / Secrets Manager)
├── dev.py                  # Typer CLI entry point (jira-cli script)
├── src/
│   ├── app.py              # Lambda handler (query-offboarding-tickets, query-overdue-tickets, add-comment, add-due-date, add-label)
│   ├── constants.py        # JQLQueries, JiraLabels, RegexPatterns
│   ├── jira_client.py      # Jira REST API client with retry logic
│   ├── secrets_manager.py  # AWS Secrets Manager client
│   └── utils.py            # ADF parsing and text utilities
└── tests/
    ├── unit/               # Fast, isolated unit tests
    └── integration/        # Handler-level tests with mocked HTTP
```
