renovate 43.263.8 → 43.263.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -66,6 +66,14 @@ var IssueService = class {
|
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether the project's process defines the `Issue` work item type. Some
|
|
71
|
+
* processes (e.g. Scrum) do not, in which case creating one fails; checking
|
|
72
|
+
* up front lets us log an actionable message instead of a cryptic error.
|
|
73
|
+
*/
|
|
74
|
+
async hasWorkItemType(azureApiWit) {
|
|
75
|
+
return (await azureApiWit.getWorkItemTypes(this.config.project)).some((t) => t.name === workItemType);
|
|
76
|
+
}
|
|
69
77
|
async getIssueList(titleFilter) {
|
|
70
78
|
logger.debug("getIssueList()");
|
|
71
79
|
try {
|
|
@@ -211,6 +219,10 @@ var IssueService = class {
|
|
|
211
219
|
return "updated";
|
|
212
220
|
}
|
|
213
221
|
}
|
|
222
|
+
if (!await this.hasWorkItemType(azureApiWit)) {
|
|
223
|
+
logger.warn(`Azure: work item type '${workItemType}' does not exist in project '${this.config.project}' (or the token lacks permission to it); skipping issue. The Dependency Dashboard needs a process that defines the '${workItemType}' work item type.`);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
214
226
|
const newWorkItem = await azureApiWit.createWorkItem(void 0, [
|
|
215
227
|
{
|
|
216
228
|
op: "add",
|
|
@@ -233,6 +245,10 @@ var IssueService = class {
|
|
|
233
245
|
value: "Markdown"
|
|
234
246
|
}
|
|
235
247
|
], this.config.project, workItemType);
|
|
248
|
+
if (!newWorkItem?.id) {
|
|
249
|
+
logger.warn("Azure: work item creation returned no result; skipping issue");
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
236
252
|
logger.debug(`Created new issue #${newWorkItem.id}`);
|
|
237
253
|
return "created";
|
|
238
254
|
} catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issue.js","names":["azureApi.workItemTrackingApi"],"sources":["../../../../lib/modules/platform/azure/issue.ts"],"sourcesContent":["import type {\n WorkItem,\n WorkItemStateColor,\n} from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js';\nimport { logger } from '../../../logger/index.ts';\nimport { Lazy } from '../../../util/lazy.ts';\nimport { sanitize } from '../../../util/sanitize.ts';\nimport type { EnsureIssueConfig, EnsureIssueResult, Issue } from '../types.ts';\nimport * as azureApi from './azure-got-wrapper.ts';\nimport type { Config } from './types.ts';\nimport { getWorkItemTitle } from './util.ts';\n\nconst workItemType = 'Issue';\n\n// Historical fallbacks, used when the work item type's states cannot be\n// resolved from the API (e.g. Azure DevOps Server versions without the\n// endpoint). These match the values Renovate hardcoded previously.\nconst defaultOpenState = 'New';\nconst defaultClosedState = 'Closed';\n\n// Azure Boards groups every work item state into one of these meta-state\n// categories. Which concrete state name maps to a category depends on the\n// project's process (Basic: To Do/Doing/Done, Agile: New/Active/.../Closed,\n// custom inherited processes: anything). Categories are stable, so we resolve\n// the concrete names from them instead of hardcoding process-specific values.\nconst openCategories = ['Proposed', 'InProgress'];\nconst closedCategories = ['Completed', 'Resolved', 'Removed'];\n\ninterface WorkItemStates {\n /** State to move a work item to when (re)opening it. */\n open: string;\n /** State to move a work item to when closing it. */\n closed: string;\n /** All state names that represent a closed/completed work item. */\n closedNames: Set<string>;\n}\n\n/**\n * Return the names of the states whose category is one of `categories`,\n * preserving the order Azure DevOps returns them in (workflow order).\n */\nfunction namesByCategory(\n stateColors: WorkItemStateColor[],\n categories: string[],\n): string[] {\n return stateColors\n .filter((s) => s.category && categories.includes(s.category) && s.name)\n .map((s) => s.name!);\n}\n\nexport class IssueService {\n private config: Config;\n private readonly workItemStates: Lazy<Promise<WorkItemStates>>;\n\n constructor(config: Config) {\n this.config = config;\n // Wrapped in `Lazy` so concurrent callers share a single resolution\n // instead of each firing a duplicate `getWorkItemTypeStates` request.\n this.workItemStates = new Lazy(() => this.resolveWorkItemStates());\n }\n\n /**\n * Resolve the concrete open/closed state names for the `Issue` work item type\n * from its process, so Renovate does not depend on hardcoded state names that\n * only exist in some Azure DevOps processes. Falls back to the historical\n * `New`/`Closed` values if the states cannot be fetched.\n */\n private async resolveWorkItemStates(): Promise<WorkItemStates> {\n const states: WorkItemStates = {\n open: defaultOpenState,\n closed: defaultClosedState,\n closedNames: new Set([defaultClosedState]),\n };\n\n try {\n const azureApiWit = await azureApi.workItemTrackingApi();\n const stateColors = await azureApiWit.getWorkItemTypeStates(\n this.config.project,\n workItemType,\n );\n\n if (stateColors?.length) {\n const openNames = namesByCategory(stateColors, openCategories);\n // A `Completed` state (e.g. Agile's `Closed`) is the true \"done\" state.\n // `Resolved`/`Removed` states also count as closed but must not be\n // picked over `Completed` when moving an item to closed.\n const completed = stateColors.find(\n (s) => s.category === 'Completed' && s.name,\n );\n const closedNames = namesByCategory(stateColors, closedCategories);\n\n // First open-category state, else the type's first state overall. If a\n // process somehow exposes only closed states, this falls back to a\n // closed state name; that is intentional (there is no better reopen\n // target) rather than a bug.\n states.open = openNames[0] ?? stateColors[0].name ?? states.open;\n if (closedNames.length) {\n // Prefer a `Completed` state as the close target so processes that\n // order `Resolved` before `Closed` (e.g. some custom Agile\n // inheritances) still close to `Closed`; fall back to the wider set\n // only when no `Completed` state exists. Keep the full set for\n // recognising already-closed items.\n states.closed = completed?.name ?? closedNames[0];\n states.closedNames = new Set(closedNames);\n }\n }\n } catch (err) {\n logger.debug(\n { err },\n 'Azure: could not resolve work item states, using default state names',\n );\n }\n\n return states;\n }\n\n async findIssue(title: string): Promise<Issue | null> {\n logger.debug(`findIssue(${title})`);\n try {\n const finalTitle = getWorkItemTitle(title, this.config.repository);\n const issues = await this.getIssueList(finalTitle);\n return issues[0] ?? null;\n } catch (err) {\n logger.error({ err }, 'Error finding issue');\n return null;\n }\n }\n\n async getIssueList(titleFilter?: string): Promise<Issue[]> {\n logger.debug('getIssueList()');\n try {\n const azureApiWit = await azureApi.workItemTrackingApi();\n\n let wiql = `\n SELECT [System.Id]\n FROM WorkItems\n WHERE [System.WorkItemType] = '${workItemType}'\n AND [System.TeamProject] = '${this.config.project}'\n `;\n\n if (titleFilter) {\n const escapedTitle = titleFilter.replace(/'/g, \"''\");\n wiql += ` AND [System.Title] = '${escapedTitle}'`;\n }\n\n const result = await azureApiWit.queryByWiql({ query: wiql });\n\n if (!result.workItems?.length) {\n logger.debug('getIssueList() no work items found');\n return [];\n }\n\n const workItemIds = result.workItems.map((wi: WorkItem) => wi.id!);\n const workItems = await azureApiWit.getWorkItems(workItemIds, [\n 'System.Id',\n 'System.Title',\n 'System.State',\n 'System.Description',\n 'System.CreatedDate',\n 'System.ChangedDate',\n ]);\n\n const { closedNames } = await this.workItemStates.getValue();\n\n return workItems.map((wi: WorkItem) => ({\n number: wi.id!,\n title: wi.fields!['System.Title'],\n state: closedNames.has(wi.fields!['System.State']) ? 'closed' : 'open',\n body: wi.fields!['System.Description'],\n createdAt: wi.fields!['System.CreatedDate'],\n lastModified: wi.fields!['System.ChangedDate'],\n }));\n } catch (err) {\n logger.warn({ err }, 'Error fetching issue list');\n return [];\n }\n }\n\n async ensureIssueClosing(title: string): Promise<void> {\n logger.debug(`ensureIssueClosing(${title})`);\n try {\n const issue = await this.findIssue(title);\n if (issue?.state === 'open' && issue.number) {\n const azureApiWit = await azureApi.workItemTrackingApi();\n const { closed } = await this.workItemStates.getValue();\n await azureApiWit.updateWorkItem(\n undefined,\n [{ op: 'replace', path: '/fields/System.State', value: closed }],\n issue.number,\n this.config.project,\n );\n logger.debug(`Closed issue #${issue.number}: ${title}`);\n }\n } catch (err) {\n logger.error({ err }, 'Error closing issue');\n }\n }\n\n async ensureIssue({\n title,\n body,\n once = false,\n shouldReOpen = true,\n }: EnsureIssueConfig): Promise<EnsureIssueResult | null> {\n logger.debug(`ensureIssue()`);\n\n try {\n const azureApiWit = await azureApi.workItemTrackingApi();\n const finalTitle = getWorkItemTitle(title, this.config.repository);\n const issues = await this.getIssueList(finalTitle);\n const { open, closed } = await this.workItemStates.getValue();\n\n // Close duplicate open issues if any\n const openIssues = issues.filter((issue) => issue.state === 'open');\n if (openIssues.length > 1) {\n for (let i = 1; i < openIssues.length; i++) {\n const issueNumber = openIssues[i].number;\n if (issueNumber) {\n await azureApiWit.updateWorkItem(\n undefined,\n [\n {\n op: 'replace',\n path: '/fields/System.State',\n value: closed,\n },\n ],\n issueNumber,\n this.config.project,\n );\n logger.info(`Closed duplicate issue #${issueNumber}`);\n }\n }\n }\n\n const existingIssue =\n openIssues[0] ?? issues.find((issue) => issue.state === 'closed');\n\n if (existingIssue) {\n if (existingIssue.state === 'closed' && once) {\n logger.debug('Issue already closed - skipping update');\n return null;\n }\n if (existingIssue.state === 'closed' && shouldReOpen) {\n // Reopen and update work item\n if (!existingIssue.number) {\n logger.warn('Cannot reopen issue without number');\n return null;\n }\n await azureApiWit.updateWorkItem(\n undefined,\n [\n { op: 'replace', path: '/fields/System.State', value: open },\n {\n op: 'replace',\n path: '/fields/System.Title',\n value: finalTitle,\n },\n {\n op: 'replace',\n path: '/fields/System.Description',\n value: sanitize(body),\n },\n {\n op: 'replace',\n path: '/multilineFieldsFormat/System.Description',\n value: 'Markdown',\n },\n ],\n existingIssue.number,\n this.config.project,\n );\n logger.debug(`Reopened issue #${existingIssue.number}`);\n return 'updated';\n }\n if (existingIssue.state === 'open') {\n // Update work item if needed\n if (\n existingIssue.title !== finalTitle ||\n existingIssue.body !== body\n ) {\n if (!existingIssue.number) {\n logger.warn('Cannot update issue without number');\n return null;\n }\n await azureApiWit.updateWorkItem(\n undefined,\n [\n {\n op: 'replace',\n path: '/fields/System.Title',\n value: finalTitle,\n },\n {\n op: 'replace',\n path: '/fields/System.Description',\n value: sanitize(body),\n },\n {\n op: 'replace',\n path: '/multilineFieldsFormat/System.Description',\n value: 'Markdown',\n },\n ],\n existingIssue.number,\n this.config.project,\n );\n logger.debug(`Updated issue #${existingIssue.number}`);\n return 'updated';\n }\n logger.debug(`Issue #${existingIssue.number} is already up-to-date`);\n return 'updated';\n }\n }\n\n // Create new work item if none found. System.State is intentionally\n // omitted so Azure DevOps applies the work item type's default initial\n // state for the project's process (e.g. `To Do` on Basic, `New`/`Active`\n // on Agile). Passing a hardcoded state fails on processes that lack it.\n const newWorkItem = await azureApiWit.createWorkItem(\n undefined,\n [\n {\n op: 'add',\n path: '/fields/System.WorkItemType',\n value: workItemType,\n },\n { op: 'add', path: '/fields/System.Title', value: finalTitle },\n {\n op: 'add',\n path: '/fields/System.Description',\n value: sanitize(body),\n },\n {\n op: 'add',\n path: '/multilineFieldsFormat/System.Description',\n value: 'Markdown',\n },\n ],\n this.config.project,\n workItemType,\n );\n\n logger.debug(`Created new issue #${newWorkItem.id}`);\n return 'created';\n } catch (err) {\n logger.warn({ err }, 'Error ensuring issue');\n return null;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"issue.js","names":["azureApi.workItemTrackingApi"],"sources":["../../../../lib/modules/platform/azure/issue.ts"],"sourcesContent":["import type {\n WorkItem,\n WorkItemStateColor,\n} from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js';\nimport type { IWorkItemTrackingApi } from 'azure-devops-node-api/WorkItemTrackingApi.js';\nimport { logger } from '../../../logger/index.ts';\nimport { Lazy } from '../../../util/lazy.ts';\nimport { sanitize } from '../../../util/sanitize.ts';\nimport type { EnsureIssueConfig, EnsureIssueResult, Issue } from '../types.ts';\nimport * as azureApi from './azure-got-wrapper.ts';\nimport type { Config } from './types.ts';\nimport { getWorkItemTitle } from './util.ts';\n\nconst workItemType = 'Issue';\n\n// Historical fallbacks, used when the work item type's states cannot be\n// resolved from the API (e.g. Azure DevOps Server versions without the\n// endpoint). These match the values Renovate hardcoded previously.\nconst defaultOpenState = 'New';\nconst defaultClosedState = 'Closed';\n\n// Azure Boards groups every work item state into one of these meta-state\n// categories. Which concrete state name maps to a category depends on the\n// project's process (Basic: To Do/Doing/Done, Agile: New/Active/.../Closed,\n// custom inherited processes: anything). Categories are stable, so we resolve\n// the concrete names from them instead of hardcoding process-specific values.\nconst openCategories = ['Proposed', 'InProgress'];\nconst closedCategories = ['Completed', 'Resolved', 'Removed'];\n\ninterface WorkItemStates {\n /** State to move a work item to when (re)opening it. */\n open: string;\n /** State to move a work item to when closing it. */\n closed: string;\n /** All state names that represent a closed/completed work item. */\n closedNames: Set<string>;\n}\n\n/**\n * Return the names of the states whose category is one of `categories`,\n * preserving the order Azure DevOps returns them in (workflow order).\n */\nfunction namesByCategory(\n stateColors: WorkItemStateColor[],\n categories: string[],\n): string[] {\n return stateColors\n .filter((s) => s.category && categories.includes(s.category) && s.name)\n .map((s) => s.name!);\n}\n\nexport class IssueService {\n private config: Config;\n private readonly workItemStates: Lazy<Promise<WorkItemStates>>;\n\n constructor(config: Config) {\n this.config = config;\n // Wrapped in `Lazy` so concurrent callers share a single resolution\n // instead of each firing a duplicate `getWorkItemTypeStates` request.\n this.workItemStates = new Lazy(() => this.resolveWorkItemStates());\n }\n\n /**\n * Resolve the concrete open/closed state names for the `Issue` work item type\n * from its process, so Renovate does not depend on hardcoded state names that\n * only exist in some Azure DevOps processes. Falls back to the historical\n * `New`/`Closed` values if the states cannot be fetched.\n */\n private async resolveWorkItemStates(): Promise<WorkItemStates> {\n const states: WorkItemStates = {\n open: defaultOpenState,\n closed: defaultClosedState,\n closedNames: new Set([defaultClosedState]),\n };\n\n try {\n const azureApiWit = await azureApi.workItemTrackingApi();\n const stateColors = await azureApiWit.getWorkItemTypeStates(\n this.config.project,\n workItemType,\n );\n\n if (stateColors?.length) {\n const openNames = namesByCategory(stateColors, openCategories);\n // A `Completed` state (e.g. Agile's `Closed`) is the true \"done\" state.\n // `Resolved`/`Removed` states also count as closed but must not be\n // picked over `Completed` when moving an item to closed.\n const completed = stateColors.find(\n (s) => s.category === 'Completed' && s.name,\n );\n const closedNames = namesByCategory(stateColors, closedCategories);\n\n // First open-category state, else the type's first state overall. If a\n // process somehow exposes only closed states, this falls back to a\n // closed state name; that is intentional (there is no better reopen\n // target) rather than a bug.\n states.open = openNames[0] ?? stateColors[0].name ?? states.open;\n if (closedNames.length) {\n // Prefer a `Completed` state as the close target so processes that\n // order `Resolved` before `Closed` (e.g. some custom Agile\n // inheritances) still close to `Closed`; fall back to the wider set\n // only when no `Completed` state exists. Keep the full set for\n // recognising already-closed items.\n states.closed = completed?.name ?? closedNames[0];\n states.closedNames = new Set(closedNames);\n }\n }\n } catch (err) {\n logger.debug(\n { err },\n 'Azure: could not resolve work item states, using default state names',\n );\n }\n\n return states;\n }\n\n async findIssue(title: string): Promise<Issue | null> {\n logger.debug(`findIssue(${title})`);\n try {\n const finalTitle = getWorkItemTitle(title, this.config.repository);\n const issues = await this.getIssueList(finalTitle);\n return issues[0] ?? null;\n } catch (err) {\n logger.error({ err }, 'Error finding issue');\n return null;\n }\n }\n\n /**\n * Whether the project's process defines the `Issue` work item type. Some\n * processes (e.g. Scrum) do not, in which case creating one fails; checking\n * up front lets us log an actionable message instead of a cryptic error.\n */\n private async hasWorkItemType(\n azureApiWit: IWorkItemTrackingApi,\n ): Promise<boolean> {\n const types = await azureApiWit.getWorkItemTypes(this.config.project);\n return types.some((t) => t.name === workItemType);\n }\n\n async getIssueList(titleFilter?: string): Promise<Issue[]> {\n logger.debug('getIssueList()');\n try {\n const azureApiWit = await azureApi.workItemTrackingApi();\n\n let wiql = `\n SELECT [System.Id]\n FROM WorkItems\n WHERE [System.WorkItemType] = '${workItemType}'\n AND [System.TeamProject] = '${this.config.project}'\n `;\n\n if (titleFilter) {\n const escapedTitle = titleFilter.replace(/'/g, \"''\");\n wiql += ` AND [System.Title] = '${escapedTitle}'`;\n }\n\n const result = await azureApiWit.queryByWiql({ query: wiql });\n\n if (!result.workItems?.length) {\n logger.debug('getIssueList() no work items found');\n return [];\n }\n\n const workItemIds = result.workItems.map((wi: WorkItem) => wi.id!);\n const workItems = await azureApiWit.getWorkItems(workItemIds, [\n 'System.Id',\n 'System.Title',\n 'System.State',\n 'System.Description',\n 'System.CreatedDate',\n 'System.ChangedDate',\n ]);\n\n const { closedNames } = await this.workItemStates.getValue();\n\n return workItems.map((wi: WorkItem) => ({\n number: wi.id!,\n title: wi.fields!['System.Title'],\n state: closedNames.has(wi.fields!['System.State']) ? 'closed' : 'open',\n body: wi.fields!['System.Description'],\n createdAt: wi.fields!['System.CreatedDate'],\n lastModified: wi.fields!['System.ChangedDate'],\n }));\n } catch (err) {\n logger.warn({ err }, 'Error fetching issue list');\n return [];\n }\n }\n\n async ensureIssueClosing(title: string): Promise<void> {\n logger.debug(`ensureIssueClosing(${title})`);\n try {\n const issue = await this.findIssue(title);\n if (issue?.state === 'open' && issue.number) {\n const azureApiWit = await azureApi.workItemTrackingApi();\n const { closed } = await this.workItemStates.getValue();\n await azureApiWit.updateWorkItem(\n undefined,\n [{ op: 'replace', path: '/fields/System.State', value: closed }],\n issue.number,\n this.config.project,\n );\n logger.debug(`Closed issue #${issue.number}: ${title}`);\n }\n } catch (err) {\n logger.error({ err }, 'Error closing issue');\n }\n }\n\n async ensureIssue({\n title,\n body,\n once = false,\n shouldReOpen = true,\n }: EnsureIssueConfig): Promise<EnsureIssueResult | null> {\n logger.debug(`ensureIssue()`);\n\n try {\n const azureApiWit = await azureApi.workItemTrackingApi();\n const finalTitle = getWorkItemTitle(title, this.config.repository);\n const issues = await this.getIssueList(finalTitle);\n const { open, closed } = await this.workItemStates.getValue();\n\n // Close duplicate open issues if any\n const openIssues = issues.filter((issue) => issue.state === 'open');\n if (openIssues.length > 1) {\n for (let i = 1; i < openIssues.length; i++) {\n const issueNumber = openIssues[i].number;\n if (issueNumber) {\n await azureApiWit.updateWorkItem(\n undefined,\n [\n {\n op: 'replace',\n path: '/fields/System.State',\n value: closed,\n },\n ],\n issueNumber,\n this.config.project,\n );\n logger.info(`Closed duplicate issue #${issueNumber}`);\n }\n }\n }\n\n const existingIssue =\n openIssues[0] ?? issues.find((issue) => issue.state === 'closed');\n\n if (existingIssue) {\n if (existingIssue.state === 'closed' && once) {\n logger.debug('Issue already closed - skipping update');\n return null;\n }\n if (existingIssue.state === 'closed' && shouldReOpen) {\n // Reopen and update work item\n if (!existingIssue.number) {\n logger.warn('Cannot reopen issue without number');\n return null;\n }\n await azureApiWit.updateWorkItem(\n undefined,\n [\n { op: 'replace', path: '/fields/System.State', value: open },\n {\n op: 'replace',\n path: '/fields/System.Title',\n value: finalTitle,\n },\n {\n op: 'replace',\n path: '/fields/System.Description',\n value: sanitize(body),\n },\n {\n op: 'replace',\n path: '/multilineFieldsFormat/System.Description',\n value: 'Markdown',\n },\n ],\n existingIssue.number,\n this.config.project,\n );\n logger.debug(`Reopened issue #${existingIssue.number}`);\n return 'updated';\n }\n if (existingIssue.state === 'open') {\n // Update work item if needed\n if (\n existingIssue.title !== finalTitle ||\n existingIssue.body !== body\n ) {\n if (!existingIssue.number) {\n logger.warn('Cannot update issue without number');\n return null;\n }\n await azureApiWit.updateWorkItem(\n undefined,\n [\n {\n op: 'replace',\n path: '/fields/System.Title',\n value: finalTitle,\n },\n {\n op: 'replace',\n path: '/fields/System.Description',\n value: sanitize(body),\n },\n {\n op: 'replace',\n path: '/multilineFieldsFormat/System.Description',\n value: 'Markdown',\n },\n ],\n existingIssue.number,\n this.config.project,\n );\n logger.debug(`Updated issue #${existingIssue.number}`);\n return 'updated';\n }\n logger.debug(`Issue #${existingIssue.number} is already up-to-date`);\n return 'updated';\n }\n }\n\n // Create new work item if none found. System.State is intentionally\n // omitted so Azure DevOps applies the work item type's default initial\n // state for the project's process (e.g. `To Do` on Basic, `New`/`Active`\n // on Agile). Passing a hardcoded state fails on processes that lack it.\n\n // The `Issue` work item type only exists in some processes (Basic, Agile,\n // CMMI) but not others (e.g. Scrum). Creating one on a process without it\n // returns a 404 that the REST client surfaces as `null`, so check first\n // and log an actionable message instead of failing cryptically.\n if (!(await this.hasWorkItemType(azureApiWit))) {\n logger.warn(\n `Azure: work item type '${workItemType}' does not exist in project '${this.config.project}' (or the token lacks permission to it); skipping issue. The Dependency Dashboard needs a process that defines the '${workItemType}' work item type.`,\n );\n return null;\n }\n\n const newWorkItem = await azureApiWit.createWorkItem(\n undefined,\n [\n {\n op: 'add',\n path: '/fields/System.WorkItemType',\n value: workItemType,\n },\n { op: 'add', path: '/fields/System.Title', value: finalTitle },\n {\n op: 'add',\n path: '/fields/System.Description',\n value: sanitize(body),\n },\n {\n op: 'add',\n path: '/multilineFieldsFormat/System.Description',\n value: 'Markdown',\n },\n ],\n this.config.project,\n workItemType,\n );\n\n // Azure DevOps normally returns the created work item, but the\n // underlying REST client resolves to `null` instead of throwing for\n // some responses: a 404, or any 2xx with an empty/non-JSON body (e.g. a\n // 203 sign-in page from an expired token or an SSO/proxy in front of the\n // instance). Guard the result so such a response does not crash the whole\n // run with `Cannot read properties of null (reading 'id')`.\n if (!newWorkItem?.id) {\n logger.warn(\n 'Azure: work item creation returned no result; skipping issue',\n );\n return null;\n }\n\n logger.debug(`Created new issue #${newWorkItem.id}`);\n return 'created';\n } catch (err) {\n logger.warn({ err }, 'Error ensuring issue');\n return null;\n }\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/renovate-schema.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$id": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
-
"title": "JSON schema for Renovate 43.263.
|
|
3
|
+
"title": "JSON schema for Renovate 43.263.9 config files (https://renovatebot.com/)",
|
|
4
4
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
5
|
-
"x-renovate-version": "43.263.
|
|
5
|
+
"x-renovate-version": "43.263.9",
|
|
6
6
|
"allowComments": true,
|
|
7
7
|
"type": "object",
|
|
8
8
|
"definitions": {
|