Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 47 additions & 3 deletions apps/cli/src/commands/jira.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
fail,
getActiveProfile,
getFlag,
getFlags,
hasFlag,
loadConfig,
output,
Expand Down Expand Up @@ -328,6 +329,43 @@ Options:

// ============ Issue Operations ============

/**
* Parses repeated --field key=value flags into a fields object suitable for the Jira API.
*
* Type coercion rules:
* - "null" → null
* - numeric strings → number
* - valid JSON → parsed value (enables objects like {"value":"High"} and arrays)
* - everything else → string
*
* Examples:
* --field customfield_10028=5 → { customfield_10028: 5 }
* --field customfield_10077={"value":"Feature"} → { customfield_10077: { value: "Feature" } }
* --field customfield_10194=Some text → { customfield_10194: "Some text" }
*/
function parseCustomFields(rawFields: string[]): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const raw of rawFields) {
const eqIdx = raw.indexOf("=");
if (eqIdx === -1) continue;
const key = raw.slice(0, eqIdx).trim();
const value = raw.slice(eqIdx + 1);
if (!key) continue;
if (value === "null") {
result[key] = null;
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
result[key] = parseFloat(value);
} else {
try {
result[key] = JSON.parse(value);
} catch {
result[key] = value;
}
}
}
return result;
}

async function handleIssue(
args: string[],
flags: Record<string, string | boolean | string[]>,
Expand Down Expand Up @@ -412,6 +450,7 @@ async function handleIssueCreate(
const assignee = getFlag(flags, "assignee");
const labels = getFlag(flags, "labels");
const parent = getFlag(flags, "parent"); // For subtasks or epic children
const customFields = parseCustomFields(getFlags(flags, "field"));

if (!project || !type || !summary) {
fail(opts, 1, ERROR_CODES.USAGE, "--project, --type, and --summary are required (or set defaults.project in config).");
Expand All @@ -426,6 +465,7 @@ async function handleIssueCreate(
assignee: assignee ? { accountId: assignee } : undefined,
labels: labels ? labels.split(",").map((l) => l.trim()) : undefined,
parent: parent ? { key: parent } : undefined,
...customFields,
},
});

Expand All @@ -447,10 +487,11 @@ async function handleIssueUpdate(
const assignee = getFlag(flags, "assignee");
const addLabels = getFlag(flags, "add-labels");
const removeLabels = getFlag(flags, "remove-labels");
const customFields = parseCustomFields(getFlags(flags, "field"));

const client = await getClient(flags, opts);

const fields: Record<string, unknown> = {};
const fields: Record<string, unknown> = { ...customFields };
if (summary) fields.summary = summary;
if (description) fields.description = client.textToAdf(description);
if (priority) fields.priority = { name: priority };
Expand Down Expand Up @@ -722,8 +763,8 @@ function issueHelp(): string {

Commands:
get --key <key> [--expand <fields>]
create --project <key> --type <name> --summary <text> [--description <text>] [--priority <name>] [--assignee <accountId>] [--labels <a,b,c>] [--parent <key>]
update --key <key> [--summary <text>] [--description <text>] [--priority <name>] [--assignee <accountId>|none] [--add-labels <a,b>] [--remove-labels <c,d>]
create --project <key> --type <name> --summary <text> [--description <text>] [--priority <name>] [--assignee <accountId>] [--labels <a,b,c>] [--parent <key>] [--field <id>=<value> ...]
update --key <key> [--summary <text>] [--description <text>] [--priority <name>] [--assignee <accountId>|none] [--add-labels <a,b>] [--remove-labels <c,d>] [--field <id>=<value> ...]
delete --key <key> --confirm [--delete-subtasks]
transition --key <key> --to <status>
transitions --key <key> List available transitions
Expand All @@ -740,6 +781,9 @@ Options:
--profile <name> Use a specific auth profile
--json JSON output
--comment Add comment when linking (link-page only)
--field <id>=<value> Set a custom field (repeatable). Value is auto-coerced:
numbers → number, JSON strings → parsed, plain text → string.
Example: --field customfield_10028=5 --field customfield_10077={"value":"Bug"}
`;
}

Expand Down
13 changes: 10 additions & 3 deletions src/content/docs/jira/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,24 @@ ID VALUE

### Set Custom Fields

When creating or updating issues:
Use the `--field <id>=<value>` flag on `issue create` and `issue update`. The flag is repeatable for multiple fields:

```bash
# Create with custom field
atlcli jira issue create --project PROJ --type Story --summary "Feature" \
--set "customfield_10001=5"
--field customfield_10001=5

# Update custom field
atlcli jira issue update --key PROJ-123 --set "customfield_10001=8"
atlcli jira issue update --key PROJ-123 --field customfield_10001=8

# Set multiple fields at once
atlcli jira issue update --key PROJ-123 \
--field customfield_10001=8 \
--field customfield_10002='{"value":"Backend"}'
```

See [Issues → Custom Fields](issues.md#custom-fields) for full type coercion rules and examples.

### Query by Custom Fields

Use JQL to search by custom field values:
Expand Down
60 changes: 55 additions & 5 deletions src/content/docs/jira/issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Options:
| `--priority` | Priority name |
| `--labels` | Comma-separated labels |
| `--parent` | Parent issue key (for subtasks) |
| `--field` | Set a custom field — `<id>=<value>`, repeatable |

### Examples

Expand All @@ -75,6 +76,59 @@ Use `atlcli jira epic add` or `atlcli jira sprint add` after creating the issue.

:::

## Custom Fields

Use `--field <id>=<value>` to set custom fields when creating or updating issues. The flag can be repeated for multiple fields.

```bash
atlcli jira issue create --project PROJ --type Story --summary "My story" \
--field customfield_10028=5 \
--field customfield_10077='{"value":"Feature"}' \
--field customfield_10194="As a user I want to..."

atlcli jira issue update --key PROJ-123 \
--field customfield_10028=8 \
--field customfield_10079=3
```

### Value Type Coercion

Values are automatically coerced to the correct type:

| Input | Resulting type | Example |
|-------|----------------|---------|
| Numeric string | `number` | `--field customfield_10028=5` |
| `null` | `null` | `--field customfield_10028=null` |
| Valid JSON | Parsed value | `--field customfield_10077='{"value":"Bug"}'` |
| Everything else | `string` | `--field customfield_10194="Some text"` |

Use JSON syntax for Jira option, multi-select, and array fields:

```bash
# Single select (option)
--field customfield_10077='{"value":"Feature"}'

# Multi-select (array of options)
--field customfield_10195='[{"value":"Goal A"},{"value":"Goal B"}]'

# User field
--field customfield_10050='{"accountId":"557058:abc123"}'
```

### Finding Field IDs

Use `atlcli jira field search` to look up the ID for a custom field:

```bash
atlcli jira field search "story points"
atlcli jira field options customfield_10077 # list allowed option values
```

:::note[Field availability]
Custom fields must be on the issue's create or edit screen in Jira. If you receive a "field cannot be set" error, the field is not configured for that issue type or project screen.

:::

## Update Issue

```bash
Expand All @@ -92,6 +146,7 @@ Options:
| `--add-labels` | Add labels (comma-separated) |
| `--remove-labels` | Remove labels (comma-separated) |
| `--assignee` | New assignee (account ID or `none` to unassign) |
| `--field` | Set a custom field — `<id>=<value>`, repeatable |

### Examples

Expand All @@ -108,11 +163,6 @@ atlcli jira issue update --key PROJ-123 --add-labels reviewed,verified
atlcli jira issue update --key PROJ-123 --assignee none
```

:::tip[Setting Custom Fields]
Use `atlcli jira bulk edit --issues PROJ-123 --set field=value` to set custom fields on a single issue.

:::

## Delete Issue

```bash
Expand Down