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
69 changes: 69 additions & 0 deletions clis/jira/commands.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,75 @@ describe('jira commands', () => {
expect(rows[0].linkedIssues[0]).toEqual({ key: 'PROJ-2', type: 'Blocks', direction: 'outward' });
});

it('uses --fields and resolves custom field names', async () => {
setCloudEnv();
vi.stubGlobal('fetch', vi.fn(async (url) => {
const parsed = new URL(String(url));
expect(parsed.searchParams.get('fields')).toBe('summary,status,customfield_12345');
expect(parsed.searchParams.get('expand')).toBe('renderedFields,names');
return jsonResponse({
key: 'PROJ-1',
fields: {
summary: 'Checkout fails',
status: { name: 'In Progress' },
customfield_12345: 8,
},
names: {
customfield_12345: 'Story Estimate',
},
});
}));
const cmd = getRegistry().get('jira/issue');
const rows = await cmd.func({ key: 'PROJ-1', fields: ' summary , status, customfield_12345, customfield_12345 ' });
expect(rows[0]).toMatchObject({
key: 'PROJ-1',
summary: 'Checkout fails',
status: 'In Progress',
customFields: { 'Story Estimate': 8 },
});
expect(rows[0].comments).toEqual([]);
expect(rows[0].attachments).toEqual([]);
expect(rows[0].linkedIssues).toEqual([]);
});

it('rejects empty --fields values', async () => {
setCloudEnv();
const fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
const cmd = getRegistry().get('jira/issue');
await expect(cmd.func({ key: 'PROJ-1', fields: ' , ' })).rejects.toMatchObject({ code: 'CONFIG' });
expect(fetchMock).not.toHaveBeenCalled();
});

it('fetches all fields in auto mode and cleans nulls with named custom fields', async () => {
setCloudEnv();
vi.stubGlobal('fetch', vi.fn(async (url) => {
const parsed = new URL(String(url));
expect(parsed.searchParams.has('fields')).toBe(false);
expect(parsed.searchParams.get('expand')).toBe('renderedFields,names');
return jsonResponse({
key: 'PROJ-1',
fields: {
summary: 'Checkout fails',
customfield_12345: 'Enterprise',
customfield_12346: null,
status: { name: 'In Progress' },
},
names: {
customfield_12345: 'Customer Segment',
customfield_12346: 'Unused Field',
},
});
}));
const cmd = getRegistry().get('jira/issue');
const rows = await cmd.func({ key: 'PROJ-1', fields: 'auto' });
expect(rows[0].fields).toEqual({
summary: 'Checkout fails',
'Customer Segment': 'Enterprise',
status: { name: 'In Progress' },
});
});

it('fails typed when Jira issue payload is missing stable issue identity', async () => {
setCloudEnv();
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ fields: { summary: 'No key' } })));
Expand Down
5 changes: 3 additions & 2 deletions clis/jira/issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@ cli({
args: [
{ name: 'key', positional: true, required: true, help: 'Jira issue key, e.g. PROJ-123' },
{ name: 'comments-limit', type: 'int', default: 100, help: 'Max comments to include (1-100)' },
{ name: 'fields', type: 'string', help: 'Fields to request, comma-separated, or auto for all fields' },
],
columns: ['key', 'summary', 'issueType', 'status', 'priority', 'assignee', 'updated', 'url'],
func: async (args) => {
const key = requireIssueKey(args.key);
const config = jiraConfig();
const issue = await fetchIssue(config, key);
const issue = await fetchIssue(config, key, [], args.fields);
const inlineComments = issue?.fields?.comment?.comments;
const total = Number(issue?.fields?.comment?.total ?? inlineComments?.length ?? 0);
const comments = total > (inlineComments?.length ?? 0)
? await fetchComments(config, key, args['comments-limit'])
: inlineComments;
return [normalizeJiraIssue(issue, config, { comments })];
return [normalizeJiraIssue(issue, config, { comments, fields: args.fields })];
},
});
66 changes: 57 additions & 9 deletions clis/jira/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
requirePayloadString,
requireString,
} from '../_atlassian/shared.js';
import { ArgumentError } from '@jackwener/opencli/errors';
import { ArgumentError, ConfigError } from '@jackwener/opencli/errors';

const DEFAULT_ISSUE_FIELDS = [
'summary',
Expand Down Expand Up @@ -53,7 +53,24 @@ function configuredFieldNames() {
};
}

function issueFields(extraFields = []) {
function configuredIssueFields(raw) {
if (raw === undefined) return null;
if (raw.trim().toLowerCase() === 'auto') return 'auto';

const fields = raw.split(',').map((field) => field.trim()).filter(Boolean);
if (fields.length === 0) {
throw new ConfigError(
'Invalid Jira fields',
'Set --fields to comma-separated field names, for example summary,status,customfield_12345.',
);
}
return [...new Set(fields)];
}

function issueFields(extraFields = [], fieldsOverride) {
const selected = configuredIssueFields(fieldsOverride);
if (selected === 'auto') return undefined;
if (selected !== null) return selected.join(',');
const configured = Object.values(configuredFieldNames()).filter(Boolean);
return [...new Set([...DEFAULT_ISSUE_FIELDS, ...configured, ...extraFields.filter(Boolean)])].join(',');
}
Expand Down Expand Up @@ -136,6 +153,20 @@ function customValueToMarkdown(value) {
return valueName(value);
}

function autoFields(fields, names) {
const result = {};
for (const [field, value] of Object.entries(fields)) {
if (value === null) continue;
const name = field.startsWith('customfield_')
&& typeof names[field] === 'string'
&& names[field].trim()
? names[field].trim()
: field;
result[name] = value;
}
return result;
}

function inlineComments(fields, key, options) {
if (options.comments !== undefined) return requirePayloadArray(options.comments, `jira issue ${key} comments`);
if (options.requireNestedCollections === false) return [];
Expand All @@ -150,9 +181,10 @@ export function normalizeJiraIssue(issue, config, options = {}) {
const rendered = row.renderedFields && typeof row.renderedFields === 'object' && !Array.isArray(row.renderedFields)
? row.renderedFields
: {};
const selectedFields = configuredIssueFields(options.fields);
const custom = configuredFieldNames();
const comments = inlineComments(fields, key, options);
const requireNestedCollections = options.requireNestedCollections !== false;
const requireNestedCollections = selectedFields === null && options.requireNestedCollections !== false;
const comments = inlineComments(fields, key, { ...options, requireNestedCollections });
const attachments = requireNestedCollections
? requirePayloadArray(fields.attachment, `jira issue ${key} attachment field`)
: [];
Expand Down Expand Up @@ -196,6 +228,20 @@ export function normalizeJiraIssue(issue, config, options = {}) {
if (custom.storyPoints && fields[custom.storyPoints] !== undefined) {
normalized.storyPoints = Number(fields[custom.storyPoints]);
}
if (selectedFields !== null) {
const names = row.names && typeof row.names === 'object' && !Array.isArray(row.names) ? row.names : {};
if (selectedFields === 'auto') {
normalized.fields = autoFields(fields, names);
} else {
const customFields = {};
for (const field of selectedFields) {
if (!field.startsWith('customfield_') || fields[field] === undefined) continue;
const name = typeof names[field] === 'string' && names[field].trim() ? names[field].trim() : field;
customFields[name] = fields[field];
}
if (Object.keys(customFields).length > 0) normalized.customFields = customFields;
}
}
return normalized;
}

Expand All @@ -213,12 +259,14 @@ export function issueSummaryRow(issue, config) {
};
}

export async function fetchIssue(config, key, extraFields = []) {
export async function fetchIssue(config, key, extraFields = [], fieldsOverride) {
const fields = issueFields(extraFields, fieldsOverride);
const params = {
expand: 'renderedFields,names',
};
if (fields !== undefined) params.fields = fields;
const issue = await jiraRequest(config, `/issue/${encodeURIComponent(key)}`, {
params: {
fields: issueFields(extraFields),
expand: 'renderedFields',
},
params,
label: `jira issue ${key}`,
});
return requirePayloadObject(issue, `jira issue ${key}`);
Expand Down
18 changes: 18 additions & 0 deletions docs/adapters/browser/jira.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ export ATLASSIAN_EMAIL=you@example.com
export ATLASSIAN_API_TOKEN=...
```

To request a specific set of Jira fields, pass a comma-separated list to the
`issue` command. The list replaces the default field list:

```bash
opencli jira issue PROJ-123 --fields 'summary,status,customfield_12345'
```

Requested `customfield_*` values are returned under `customFields`, using the
human-readable names returned by Jira when available.

Set `--fields` to `auto` to let Jira return all fields. The result adds a
`fields` object with top-level `null` values removed and `customfield_*` keys
replaced by their human-readable names from Jira:

```bash
opencli jira issue PROJ-123 --fields auto
```

For Data Center, use a personal access token when available:

```bash
Expand Down