9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
257 lines
9.3 KiB
JavaScript
257 lines
9.3 KiB
JavaScript
import {
|
|
adfToMarkdown,
|
|
atlassianRequest,
|
|
getJiraConfig,
|
|
htmlToMarkdown,
|
|
parseLimit,
|
|
queryString,
|
|
requireNonEmptyRows,
|
|
requirePayloadArray,
|
|
requirePayloadObject,
|
|
requirePayloadString,
|
|
requireString,
|
|
} from '../_atlassian/shared.js';
|
|
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
|
|
const DEFAULT_ISSUE_FIELDS = [
|
|
'summary',
|
|
'issuetype',
|
|
'status',
|
|
'priority',
|
|
'labels',
|
|
'description',
|
|
'comment',
|
|
'attachment',
|
|
'issuelinks',
|
|
'fixVersions',
|
|
'versions',
|
|
'components',
|
|
'project',
|
|
'reporter',
|
|
'assignee',
|
|
'created',
|
|
'updated',
|
|
];
|
|
|
|
function jiraApiPrefix(config) {
|
|
return `/rest/api/${config.deployment === 'cloud' ? '3' : '2'}`;
|
|
}
|
|
|
|
export function jiraApiPath(config, resource, params) {
|
|
return `${jiraApiPrefix(config)}${resource.startsWith('/') ? resource : `/${resource}`}${params ? queryString(params) : ''}`;
|
|
}
|
|
|
|
export async function jiraRequest(config, resource, options = {}) {
|
|
return atlassianRequest(config, jiraApiPath(config, resource, options.params), options);
|
|
}
|
|
|
|
function configuredFieldNames() {
|
|
return {
|
|
acceptanceCriteria: process.env.ATLASSIAN_JIRA_ACCEPTANCE_FIELD?.trim() || '',
|
|
sprint: process.env.ATLASSIAN_JIRA_SPRINT_FIELD?.trim() || '',
|
|
storyPoints: process.env.ATLASSIAN_JIRA_STORY_POINTS_FIELD?.trim() || '',
|
|
};
|
|
}
|
|
|
|
function issueFields(extraFields = []) {
|
|
const configured = Object.values(configuredFieldNames()).filter(Boolean);
|
|
return [...new Set([...DEFAULT_ISSUE_FIELDS, ...configured, ...extraFields.filter(Boolean)])].join(',');
|
|
}
|
|
|
|
export function parseJiraLimit(value, fallback = 20, max = 100) {
|
|
return parseLimit(value, fallback, max, 'jira limit');
|
|
}
|
|
|
|
export function requireIssueKey(value) {
|
|
const key = requireString(value, 'Jira issue key');
|
|
if (!/^[A-Za-z][A-Za-z0-9_]+-\d+$/.test(key)) {
|
|
throw new ArgumentError(`Invalid Jira issue key: ${key}`, 'Expected a key like PROJECT-123.');
|
|
}
|
|
return key.toUpperCase();
|
|
}
|
|
|
|
function displayUser(user) {
|
|
if (!user || typeof user !== 'object') return '';
|
|
return String(user.displayName ?? user.name ?? user.emailAddress ?? user.accountId ?? '');
|
|
}
|
|
|
|
function valueName(value) {
|
|
if (!value) return '';
|
|
if (typeof value === 'string') return value;
|
|
if (typeof value === 'object') return String(value.name ?? value.value ?? value.key ?? value.id ?? '');
|
|
return String(value);
|
|
}
|
|
|
|
function valueNames(values) {
|
|
return Array.isArray(values) ? values.map(valueName).filter(Boolean) : [];
|
|
}
|
|
|
|
export function jiraBodyToMarkdown(raw, rendered) {
|
|
if (rendered) return htmlToMarkdown(rendered);
|
|
if (raw && typeof raw === 'object') return adfToMarkdown(raw);
|
|
if (typeof raw === 'string') return raw.trim();
|
|
return '';
|
|
}
|
|
|
|
export function normalizeComment(comment) {
|
|
const row = requirePayloadObject(comment, 'jira comment');
|
|
return {
|
|
id: requirePayloadString(row.id, 'comment id', 'jira comment'),
|
|
author: displayUser(row.author),
|
|
created: row.created ? String(row.created) : '',
|
|
updated: row.updated ? String(row.updated) : undefined,
|
|
markdown: jiraBodyToMarkdown(row.body, row.renderedBody),
|
|
};
|
|
}
|
|
|
|
export function normalizeAttachment(attachment) {
|
|
const row = requirePayloadObject(attachment, 'jira attachment');
|
|
return {
|
|
id: requirePayloadString(row.id, 'attachment id', 'jira attachment'),
|
|
filename: requirePayloadString(row.filename, 'filename', 'jira attachment'),
|
|
mimeType: row.mimeType ? String(row.mimeType) : undefined,
|
|
size: row.size != null ? Number(row.size) : undefined,
|
|
url: requirePayloadString(row.content ?? row.self, 'attachment url', 'jira attachment'),
|
|
};
|
|
}
|
|
|
|
export function normalizeIssueLink(link) {
|
|
const row = requirePayloadObject(link, 'jira issue link');
|
|
const outward = row.outwardIssue;
|
|
const inward = row.inwardIssue;
|
|
const issue = outward ?? inward ?? {};
|
|
const key = requirePayloadString(issue.key, 'linked issue key', 'jira issue link');
|
|
return {
|
|
key,
|
|
type: String(row.type?.name ?? (outward ? row.type?.outward : row.type?.inward) ?? ''),
|
|
direction: outward ? 'outward' : 'inward',
|
|
};
|
|
}
|
|
|
|
function customValueToMarkdown(value) {
|
|
if (!value) return '';
|
|
if (typeof value === 'string') return value.trim();
|
|
if (value && typeof value === 'object' && value.type === 'doc') return adfToMarkdown(value);
|
|
if (Array.isArray(value)) return value.map(valueName).filter(Boolean).join(', ');
|
|
return valueName(value);
|
|
}
|
|
|
|
function inlineComments(fields, key, options) {
|
|
if (options.comments !== undefined) return requirePayloadArray(options.comments, `jira issue ${key} comments`);
|
|
if (options.requireNestedCollections === false) return [];
|
|
const commentBlock = requirePayloadObject(fields.comment, `jira issue ${key} comment field`);
|
|
return requirePayloadArray(commentBlock.comments, `jira issue ${key} comment field comments`);
|
|
}
|
|
|
|
export function normalizeJiraIssue(issue, config, options = {}) {
|
|
const row = requirePayloadObject(issue, 'jira issue');
|
|
const key = requirePayloadString(row.key, 'issue key', 'jira issue');
|
|
const fields = requirePayloadObject(row.fields, `jira issue ${key} fields`);
|
|
const rendered = row.renderedFields && typeof row.renderedFields === 'object' && !Array.isArray(row.renderedFields)
|
|
? row.renderedFields
|
|
: {};
|
|
const custom = configuredFieldNames();
|
|
const comments = inlineComments(fields, key, options);
|
|
const requireNestedCollections = options.requireNestedCollections !== false;
|
|
const attachments = requireNestedCollections
|
|
? requirePayloadArray(fields.attachment, `jira issue ${key} attachment field`)
|
|
: [];
|
|
const issueLinks = requireNestedCollections
|
|
? requirePayloadArray(fields.issuelinks, `jira issue ${key} issuelinks field`)
|
|
: [];
|
|
const normalized = {
|
|
key,
|
|
id: row.id != null ? String(row.id) : '',
|
|
url: `${config.baseUrl}/browse/${key}`,
|
|
summary: String(fields.summary ?? ''),
|
|
issueType: valueName(fields.issuetype) || undefined,
|
|
status: valueName(fields.status) || undefined,
|
|
priority: valueName(fields.priority) || undefined,
|
|
project: valueName(fields.project) || undefined,
|
|
reporter: displayUser(fields.reporter) || undefined,
|
|
assignee: displayUser(fields.assignee) || undefined,
|
|
labels: Array.isArray(fields.labels) ? fields.labels.map(String) : [],
|
|
description: {
|
|
raw: fields.description ?? null,
|
|
markdown: jiraBodyToMarkdown(fields.description, rendered.description),
|
|
},
|
|
comments: comments.map(normalizeComment),
|
|
attachments: attachments.map(normalizeAttachment),
|
|
linkedIssues: issueLinks.map(normalizeIssueLink),
|
|
fixVersions: valueNames(fields.fixVersions),
|
|
affectedVersions: valueNames(fields.versions),
|
|
components: valueNames(fields.components),
|
|
created: fields.created ? String(fields.created) : undefined,
|
|
updated: fields.updated ? String(fields.updated) : undefined,
|
|
};
|
|
if (custom.acceptanceCriteria && fields[custom.acceptanceCriteria] !== undefined) {
|
|
normalized.acceptanceCriteria = {
|
|
raw: fields[custom.acceptanceCriteria],
|
|
markdown: customValueToMarkdown(fields[custom.acceptanceCriteria]),
|
|
};
|
|
}
|
|
if (custom.sprint && fields[custom.sprint] !== undefined) {
|
|
normalized.sprint = customValueToMarkdown(fields[custom.sprint]);
|
|
}
|
|
if (custom.storyPoints && fields[custom.storyPoints] !== undefined) {
|
|
normalized.storyPoints = Number(fields[custom.storyPoints]);
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
export function issueSummaryRow(issue, config) {
|
|
const normalized = normalizeJiraIssue(issue, config, { comments: [], requireNestedCollections: false });
|
|
return {
|
|
key: normalized.key,
|
|
summary: normalized.summary,
|
|
issueType: normalized.issueType,
|
|
status: normalized.status,
|
|
priority: normalized.priority,
|
|
assignee: normalized.assignee,
|
|
updated: normalized.updated,
|
|
url: normalized.url,
|
|
};
|
|
}
|
|
|
|
export async function fetchIssue(config, key, extraFields = []) {
|
|
const issue = await jiraRequest(config, `/issue/${encodeURIComponent(key)}`, {
|
|
params: {
|
|
fields: issueFields(extraFields),
|
|
expand: 'renderedFields',
|
|
},
|
|
label: `jira issue ${key}`,
|
|
});
|
|
return requirePayloadObject(issue, `jira issue ${key}`);
|
|
}
|
|
|
|
export async function fetchComments(config, key, limit = 100) {
|
|
const maxResults = parseJiraLimit(limit, 100, 100);
|
|
const data = await jiraRequest(config, `/issue/${encodeURIComponent(key)}/comment`, {
|
|
params: { startAt: 0, maxResults, expand: 'renderedBody' },
|
|
label: `jira comments ${key}`,
|
|
});
|
|
const payload = requirePayloadObject(data, `jira comments ${key}`);
|
|
return requirePayloadArray(payload.comments, `jira comments ${key}`);
|
|
}
|
|
|
|
export function jiraRowsOrEmpty(rows, label, hint) {
|
|
return requireNonEmptyRows(rows, label, hint);
|
|
}
|
|
|
|
export function jiraConfig() {
|
|
return getJiraConfig();
|
|
}
|
|
|
|
export const __test__ = {
|
|
configuredFieldNames,
|
|
fetchIssue,
|
|
issueSummaryRow,
|
|
jiraApiPath,
|
|
jiraBodyToMarkdown,
|
|
normalizeAttachment,
|
|
normalizeComment,
|
|
normalizeIssueLink,
|
|
normalizeJiraIssue,
|
|
requireIssueKey,
|
|
};
|