renovate 44.25.0 → 44.26.3

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.
@@ -2,7 +2,7 @@ import { GlobalConfig } from "../../../config/global.js";
2
2
  import { sanitize } from "../../../util/sanitize.js";
3
3
  import { logger } from "../../../logger/index.js";
4
4
  import { Lazy } from "../../../util/lazy.js";
5
- import { workItemTrackingApi } from "./azure-got-wrapper.js";
5
+ import { isHosted, workItemTrackingApi } from "./azure-got-wrapper.js";
6
6
  import { getWorkItemTitle } from "./util.js";
7
7
  import { isNonEmptyString } from "@sindresorhus/is";
8
8
  //#region lib/modules/platform/azure/issue.ts
@@ -21,12 +21,27 @@ const closedCategories = [
21
21
  function namesByCategory(stateColors, categories) {
22
22
  return stateColors.filter((s) => s.category && categories.includes(s.category) && s.name).map((s) => s.name);
23
23
  }
24
+ /**
25
+ * Op that renders the issue body as Markdown. Only Azure DevOps Services
26
+ * (cloud) supports the `multilineFieldsFormat` field; Azure DevOps Server
27
+ * (on-premises) rejects it with a 400, so this is only included when the
28
+ * instance is hosted (see discussion #45068).
29
+ */
30
+ function markdownFormatOp(op) {
31
+ return {
32
+ op,
33
+ path: "/multilineFieldsFormat/System.Description",
34
+ value: "Markdown"
35
+ };
36
+ }
24
37
  var IssueService = class {
25
38
  config;
26
39
  workItemStates;
40
+ hosted;
27
41
  constructor(config) {
28
42
  this.config = config;
29
43
  this.workItemStates = new Lazy(() => this.resolveWorkItemStates());
44
+ this.hosted = new Lazy(() => isHosted());
30
45
  }
31
46
  /**
32
47
  * Resolve the concrete open/closed state names for the `Issue` work item type
@@ -142,6 +157,7 @@ var IssueService = class {
142
157
  const finalTitle = getWorkItemTitle(title, this.config.repository);
143
158
  const issues = await this.getIssueList(finalTitle);
144
159
  const { open, closed } = await this.workItemStates.getValue();
160
+ const hosted = await this.hosted.getValue();
145
161
  const openIssues = issues.filter((issue) => issue.state === "open");
146
162
  if (openIssues.length > 1) for (let i = 1; i < openIssues.length; i++) {
147
163
  const issueNumber = openIssues[i].number;
@@ -181,11 +197,7 @@ var IssueService = class {
181
197
  path: "/fields/System.Description",
182
198
  value: sanitize(body)
183
199
  },
184
- {
185
- op: "replace",
186
- path: "/multilineFieldsFormat/System.Description",
187
- value: "Markdown"
188
- }
200
+ ...hosted ? [markdownFormatOp("replace")] : []
189
201
  ], existingIssue.number, this.config.project);
190
202
  logger.debug(`Reopened issue #${existingIssue.number}`);
191
203
  return "updated";
@@ -207,11 +219,7 @@ var IssueService = class {
207
219
  path: "/fields/System.Description",
208
220
  value: sanitize(body)
209
221
  },
210
- {
211
- op: "replace",
212
- path: "/multilineFieldsFormat/System.Description",
213
- value: "Markdown"
214
- }
222
+ ...hosted ? [markdownFormatOp("replace")] : []
215
223
  ], existingIssue.number, this.config.project);
216
224
  logger.debug(`Updated issue #${existingIssue.number}`);
217
225
  return "updated";
@@ -246,11 +254,7 @@ var IssueService = class {
246
254
  path: "/fields/System.Description",
247
255
  value: sanitize(body)
248
256
  },
249
- {
250
- op: "add",
251
- path: "/multilineFieldsFormat/System.Description",
252
- value: "Markdown"
253
- }
257
+ ...hosted ? [markdownFormatOp("add")] : []
254
258
  ], this.config.project, this.config.workItemType);
255
259
  if (!newWorkItem?.id) {
256
260
  logger.warn("Azure: work item creation returned no result; skipping issue");
@@ -1 +1 @@
1
- {"version":3,"file":"issue.js","names":["azureApi.workItemTrackingApi"],"sources":["../../../../lib/modules/platform/azure/issue.ts"],"sourcesContent":["import { isNonEmptyString } from '@sindresorhus/is';\nimport 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 { GlobalConfig } from '../../../config/global.ts';\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\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 this.config.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 * The work item type names the project's process defines. Some processes\n * (e.g. Scrum) do not define `Issue`, in which case creating one fails;\n * checking up front lets us log an actionable message (including the types\n * that *are* available) instead of a cryptic error.\n */\n private async getWorkItemTypeNames(\n azureApiWit: IWorkItemTrackingApi,\n ): Promise<string[]> {\n const types = await azureApiWit.getWorkItemTypes(this.config.project);\n return types.map((t) => t.name).filter(isNonEmptyString);\n }\n\n async getIssueList(titleFilter?: string): Promise<Issue[]> {\n logger.debug('getIssueList()');\n try {\n const azureApiWit = await azureApi.workItemTrackingApi();\n\n // Intentionally not filtering by [System.WorkItemType]: the type is\n // configurable (`azureWorkItemType`) and a repo's Renovate issues have a\n // unique title, so matching on title + project finds them regardless of\n // which work item type they were created as.\n let wiql = `\n SELECT [System.Id]\n FROM WorkItems\n WHERE [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 configured work item type (default `Issue`) only exists in some\n // processes (Basic, Agile, CMMI) but not others (e.g. Scrum). Creating one\n // on a process without it returns a 404 that the REST client surfaces as\n // `null`, so check first and log an actionable message instead of failing\n // cryptically.\n const availableTypes = await this.getWorkItemTypeNames(azureApiWit);\n if (!availableTypes.includes(this.config.workItemType)) {\n logger.warn(\n {\n workItemType: this.config.workItemType,\n availableTypes,\n project: this.config.project,\n documentationUrl: `${GlobalConfig.get('productLinks').documentation}configuration-options/#azureworkitemtype`,\n },\n 'Azure: work item type does not exist in project (or the token lacks permission to it); skipping issue. The Dependency Dashboard needs a process that defines this work item type. Set one your project defines via the `azureWorkItemType` repo config option.',\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: this.config.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 this.config.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":";;;;;;;;AAkBA,MAAM,mBAAmB;AACzB,MAAM,qBAAqB;AAO3B,MAAM,iBAAiB,CAAC,YAAY,YAAY;AAChD,MAAM,mBAAmB;CAAC;CAAa;CAAY;AAAS;;;;;AAe5D,SAAS,gBACP,aACA,YACU;CACV,OAAO,YACJ,QAAQ,MAAM,EAAE,YAAY,WAAW,SAAS,EAAE,QAAQ,KAAK,EAAE,IAAI,CAAC,CACtE,KAAK,MAAM,EAAE,IAAK;AACvB;AAEA,IAAa,eAAb,MAA0B;CACxB;CACA;CAEA,YAAY,QAAgB;EAC1B,KAAK,SAAS;EAGd,KAAK,iBAAiB,IAAI,WAAW,KAAK,sBAAsB,CAAC;CACnE;;;;;;;CAQA,MAAc,wBAAiD;EAC7D,MAAM,SAAyB;GAC7B,MAAM;GACN,QAAQ;GACR,6BAAa,IAAI,IAAI,CAAC,kBAAkB,CAAC;EAC3C;EAEA,IAAI;GAEF,MAAM,cAAc,OAAM,MADAA,oBAA6B,EAAA,CACjB,sBACpC,KAAK,OAAO,SACZ,KAAK,OAAO,YACd;GAEA,IAAI,aAAa,QAAQ;IACvB,MAAM,YAAY,gBAAgB,aAAa,cAAc;IAI7D,MAAM,YAAY,YAAY,MAC3B,MAAM,EAAE,aAAa,eAAe,EAAE,IACzC;IACA,MAAM,cAAc,gBAAgB,aAAa,gBAAgB;IAMjE,OAAO,OAAO,UAAU,MAAM,YAAY,EAAE,CAAC,QAAQ,OAAO;IAC5D,IAAI,YAAY,QAAQ;KAMtB,OAAO,SAAS,WAAW,QAAQ,YAAY;KAC/C,OAAO,cAAc,IAAI,IAAI,WAAW;IAC1C;GACF;EACF,SAAS,KAAK;GACZ,OAAO,MACL,EAAE,IAAI,GACN,sEACF;EACF;EAEA,OAAO;CACT;CAEA,MAAM,UAAU,OAAsC;EACpD,OAAO,MAAM,aAAa,MAAM,EAAE;EAClC,IAAI;GACF,MAAM,aAAa,iBAAiB,OAAO,KAAK,OAAO,UAAU;GAEjE,QAAO,MADc,KAAK,aAAa,UAAU,EAAA,CACnC,MAAM;EACtB,SAAS,KAAK;GACZ,OAAO,MAAM,EAAE,IAAI,GAAG,qBAAqB;GAC3C,OAAO;EACT;CACF;;;;;;;CAQA,MAAc,qBACZ,aACmB;EAEnB,QAAO,MADa,YAAY,iBAAiB,KAAK,OAAO,OAAO,EAAA,CACvD,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,OAAO,gBAAgB;CACzD;CAEA,MAAM,aAAa,aAAwC;EACzD,OAAO,MAAM,gBAAgB;EAC7B,IAAI;GACF,MAAM,cAAc,MAAMA,oBAA6B;GAMvD,IAAI,OAAO;;;wCAGuB,KAAK,OAAO,QAAQ;;GAGtD,IAAI,aAAa;IACf,MAAM,eAAe,YAAY,QAAQ,MAAM,IAAI;IACnD,QAAQ,0BAA0B,aAAa;GACjD;GAEA,MAAM,SAAS,MAAM,YAAY,YAAY,EAAE,OAAO,KAAK,CAAC;GAE5D,IAAI,CAAC,OAAO,WAAW,QAAQ;IAC7B,OAAO,MAAM,oCAAoC;IACjD,OAAO,CAAC;GACV;GAEA,MAAM,cAAc,OAAO,UAAU,KAAK,OAAiB,GAAG,EAAG;GACjE,MAAM,YAAY,MAAM,YAAY,aAAa,aAAa;IAC5D;IACA;IACA;IACA;IACA;IACA;GACF,CAAC;GAED,MAAM,EAAE,gBAAgB,MAAM,KAAK,eAAe,SAAS;GAE3D,OAAO,UAAU,KAAK,QAAkB;IACtC,QAAQ,GAAG;IACX,OAAO,GAAG,OAAQ;IAClB,OAAO,YAAY,IAAI,GAAG,OAAQ,eAAe,IAAI,WAAW;IAChE,MAAM,GAAG,OAAQ;IACjB,WAAW,GAAG,OAAQ;IACtB,cAAc,GAAG,OAAQ;GAC3B,EAAE;EACJ,SAAS,KAAK;GACZ,OAAO,KAAK,EAAE,IAAI,GAAG,2BAA2B;GAChD,OAAO,CAAC;EACV;CACF;CAEA,MAAM,mBAAmB,OAA8B;EACrD,OAAO,MAAM,sBAAsB,MAAM,EAAE;EAC3C,IAAI;GACF,MAAM,QAAQ,MAAM,KAAK,UAAU,KAAK;GACxC,IAAI,OAAO,UAAU,UAAU,MAAM,QAAQ;IAC3C,MAAM,cAAc,MAAMA,oBAA6B;IACvD,MAAM,EAAE,WAAW,MAAM,KAAK,eAAe,SAAS;IACtD,MAAM,YAAY,eAChB,KAAA,GACA,CAAC;KAAE,IAAI;KAAW,MAAM;KAAwB,OAAO;IAAO,CAAC,GAC/D,MAAM,QACN,KAAK,OAAO,OACd;IACA,OAAO,MAAM,iBAAiB,MAAM,OAAO,IAAI,OAAO;GACxD;EACF,SAAS,KAAK;GACZ,OAAO,MAAM,EAAE,IAAI,GAAG,qBAAqB;EAC7C;CACF;CAEA,MAAM,YAAY,EAChB,OACA,MACA,OAAO,OACP,eAAe,QACwC;EACvD,OAAO,MAAM,eAAe;EAE5B,IAAI;GACF,MAAM,cAAc,MAAMA,oBAA6B;GACvD,MAAM,aAAa,iBAAiB,OAAO,KAAK,OAAO,UAAU;GACjE,MAAM,SAAS,MAAM,KAAK,aAAa,UAAU;GACjD,MAAM,EAAE,MAAM,WAAW,MAAM,KAAK,eAAe,SAAS;GAG5D,MAAM,aAAa,OAAO,QAAQ,UAAU,MAAM,UAAU,MAAM;GAClE,IAAI,WAAW,SAAS,GACtB,KAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;IAC1C,MAAM,cAAc,WAAW,EAAE,CAAC;IAClC,IAAI,aAAa;KACf,MAAM,YAAY,eAChB,KAAA,GACA,CACE;MACE,IAAI;MACJ,MAAM;MACN,OAAO;KACT,CACF,GACA,aACA,KAAK,OAAO,OACd;KACA,OAAO,KAAK,2BAA2B,aAAa;IACtD;GACF;GAGF,MAAM,gBACJ,WAAW,MAAM,OAAO,MAAM,UAAU,MAAM,UAAU,QAAQ;GAElE,IAAI,eAAe;IACjB,IAAI,cAAc,UAAU,YAAY,MAAM;KAC5C,OAAO,MAAM,wCAAwC;KACrD,OAAO;IACT;IACA,IAAI,cAAc,UAAU,YAAY,cAAc;KAEpD,IAAI,CAAC,cAAc,QAAQ;MACzB,OAAO,KAAK,oCAAoC;MAChD,OAAO;KACT;KACA,MAAM,YAAY,eAChB,KAAA,GACA;MACE;OAAE,IAAI;OAAW,MAAM;OAAwB,OAAO;MAAK;MAC3D;OACE,IAAI;OACJ,MAAM;OACN,OAAO;MACT;MACA;OACE,IAAI;OACJ,MAAM;OACN,OAAO,SAAS,IAAI;MACtB;MACA;OACE,IAAI;OACJ,MAAM;OACN,OAAO;MACT;KACF,GACA,cAAc,QACd,KAAK,OAAO,OACd;KACA,OAAO,MAAM,mBAAmB,cAAc,QAAQ;KACtD,OAAO;IACT;IACA,IAAI,cAAc,UAAU,QAAQ;KAElC,IACE,cAAc,UAAU,cACxB,cAAc,SAAS,MACvB;MACA,IAAI,CAAC,cAAc,QAAQ;OACzB,OAAO,KAAK,oCAAoC;OAChD,OAAO;MACT;MACA,MAAM,YAAY,eAChB,KAAA,GACA;OACE;QACE,IAAI;QACJ,MAAM;QACN,OAAO;OACT;OACA;QACE,IAAI;QACJ,MAAM;QACN,OAAO,SAAS,IAAI;OACtB;OACA;QACE,IAAI;QACJ,MAAM;QACN,OAAO;OACT;MACF,GACA,cAAc,QACd,KAAK,OAAO,OACd;MACA,OAAO,MAAM,kBAAkB,cAAc,QAAQ;MACrD,OAAO;KACT;KACA,OAAO,MAAM,UAAU,cAAc,OAAO,uBAAuB;KACnE,OAAO;IACT;GACF;GAYA,MAAM,iBAAiB,MAAM,KAAK,qBAAqB,WAAW;GAClE,IAAI,CAAC,eAAe,SAAS,KAAK,OAAO,YAAY,GAAG;IACtD,OAAO,KACL;KACE,cAAc,KAAK,OAAO;KAC1B;KACA,SAAS,KAAK,OAAO;KACrB,kBAAkB,GAAG,aAAa,IAAI,cAAc,CAAC,CAAC,cAAc;IACtE,GACA,gQACF;IACA,OAAO;GACT;GAEA,MAAM,cAAc,MAAM,YAAY,eACpC,KAAA,GACA;IACE;KACE,IAAI;KACJ,MAAM;KACN,OAAO,KAAK,OAAO;IACrB;IACA;KAAE,IAAI;KAAO,MAAM;KAAwB,OAAO;IAAW;IAC7D;KACE,IAAI;KACJ,MAAM;KACN,OAAO,SAAS,IAAI;IACtB;IACA;KACE,IAAI;KACJ,MAAM;KACN,OAAO;IACT;GACF,GACA,KAAK,OAAO,SACZ,KAAK,OAAO,YACd;GAQA,IAAI,CAAC,aAAa,IAAI;IACpB,OAAO,KACL,8DACF;IACA,OAAO;GACT;GAEA,OAAO,MAAM,sBAAsB,YAAY,IAAI;GACnD,OAAO;EACT,SAAS,KAAK;GACZ,OAAO,KAAK,EAAE,IAAI,GAAG,sBAAsB;GAC3C,OAAO;EACT;CACF;AACF"}
1
+ {"version":3,"file":"issue.js","names":["azureApi.isHosted","azureApi.workItemTrackingApi"],"sources":["../../../../lib/modules/platform/azure/issue.ts"],"sourcesContent":["import { isNonEmptyString } from '@sindresorhus/is';\nimport 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 { GlobalConfig } from '../../../config/global.ts';\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\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\n/**\n * Op that renders the issue body as Markdown. Only Azure DevOps Services\n * (cloud) supports the `multilineFieldsFormat` field; Azure DevOps Server\n * (on-premises) rejects it with a 400, so this is only included when the\n * instance is hosted (see discussion #45068).\n */\nfunction markdownFormatOp(op: 'add' | 'replace'): {\n op: string;\n path: string;\n value: string;\n} {\n return {\n op,\n path: '/multilineFieldsFormat/System.Description',\n value: 'Markdown',\n };\n}\n\nexport class IssueService {\n private config: Config;\n private readonly workItemStates: Lazy<Promise<WorkItemStates>>;\n private readonly hosted: Lazy<Promise<boolean>>;\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 // Same reason: resolve the deployment type once per run, not per issue.\n this.hosted = new Lazy(() => azureApi.isHosted());\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 this.config.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 * The work item type names the project's process defines. Some processes\n * (e.g. Scrum) do not define `Issue`, in which case creating one fails;\n * checking up front lets us log an actionable message (including the types\n * that *are* available) instead of a cryptic error.\n */\n private async getWorkItemTypeNames(\n azureApiWit: IWorkItemTrackingApi,\n ): Promise<string[]> {\n const types = await azureApiWit.getWorkItemTypes(this.config.project);\n return types.map((t) => t.name).filter(isNonEmptyString);\n }\n\n async getIssueList(titleFilter?: string): Promise<Issue[]> {\n logger.debug('getIssueList()');\n try {\n const azureApiWit = await azureApi.workItemTrackingApi();\n\n // Intentionally not filtering by [System.WorkItemType]: the type is\n // configurable (`azureWorkItemType`) and a repo's Renovate issues have a\n // unique title, so matching on title + project finds them regardless of\n // which work item type they were created as.\n let wiql = `\n SELECT [System.Id]\n FROM WorkItems\n WHERE [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 const hosted = await this.hosted.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 ...(hosted ? [markdownFormatOp('replace')] : []),\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 ...(hosted ? [markdownFormatOp('replace')] : []),\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 configured work item type (default `Issue`) only exists in some\n // processes (Basic, Agile, CMMI) but not others (e.g. Scrum). Creating one\n // on a process without it returns a 404 that the REST client surfaces as\n // `null`, so check first and log an actionable message instead of failing\n // cryptically.\n const availableTypes = await this.getWorkItemTypeNames(azureApiWit);\n if (!availableTypes.includes(this.config.workItemType)) {\n logger.warn(\n {\n workItemType: this.config.workItemType,\n availableTypes,\n project: this.config.project,\n documentationUrl: `${GlobalConfig.get('productLinks').documentation}configuration-options/#azureworkitemtype`,\n },\n 'Azure: work item type does not exist in project (or the token lacks permission to it); skipping issue. The Dependency Dashboard needs a process that defines this work item type. Set one your project defines via the `azureWorkItemType` repo config option.',\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: this.config.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 ...(hosted ? [markdownFormatOp('add')] : []),\n ],\n this.config.project,\n this.config.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":";;;;;;;;AAkBA,MAAM,mBAAmB;AACzB,MAAM,qBAAqB;AAO3B,MAAM,iBAAiB,CAAC,YAAY,YAAY;AAChD,MAAM,mBAAmB;CAAC;CAAa;CAAY;AAAS;;;;;AAe5D,SAAS,gBACP,aACA,YACU;CACV,OAAO,YACJ,QAAQ,MAAM,EAAE,YAAY,WAAW,SAAS,EAAE,QAAQ,KAAK,EAAE,IAAI,CAAC,CACtE,KAAK,MAAM,EAAE,IAAK;AACvB;;;;;;;AAQA,SAAS,iBAAiB,IAIxB;CACA,OAAO;EACL;EACA,MAAM;EACN,OAAO;CACT;AACF;AAEA,IAAa,eAAb,MAA0B;CACxB;CACA;CACA;CAEA,YAAY,QAAgB;EAC1B,KAAK,SAAS;EAGd,KAAK,iBAAiB,IAAI,WAAW,KAAK,sBAAsB,CAAC;EAEjE,KAAK,SAAS,IAAI,WAAWA,SAAkB,CAAC;CAClD;;;;;;;CAQA,MAAc,wBAAiD;EAC7D,MAAM,SAAyB;GAC7B,MAAM;GACN,QAAQ;GACR,6BAAa,IAAI,IAAI,CAAC,kBAAkB,CAAC;EAC3C;EAEA,IAAI;GAEF,MAAM,cAAc,OAAM,MADAC,oBAA6B,EAAA,CACjB,sBACpC,KAAK,OAAO,SACZ,KAAK,OAAO,YACd;GAEA,IAAI,aAAa,QAAQ;IACvB,MAAM,YAAY,gBAAgB,aAAa,cAAc;IAI7D,MAAM,YAAY,YAAY,MAC3B,MAAM,EAAE,aAAa,eAAe,EAAE,IACzC;IACA,MAAM,cAAc,gBAAgB,aAAa,gBAAgB;IAMjE,OAAO,OAAO,UAAU,MAAM,YAAY,EAAE,CAAC,QAAQ,OAAO;IAC5D,IAAI,YAAY,QAAQ;KAMtB,OAAO,SAAS,WAAW,QAAQ,YAAY;KAC/C,OAAO,cAAc,IAAI,IAAI,WAAW;IAC1C;GACF;EACF,SAAS,KAAK;GACZ,OAAO,MACL,EAAE,IAAI,GACN,sEACF;EACF;EAEA,OAAO;CACT;CAEA,MAAM,UAAU,OAAsC;EACpD,OAAO,MAAM,aAAa,MAAM,EAAE;EAClC,IAAI;GACF,MAAM,aAAa,iBAAiB,OAAO,KAAK,OAAO,UAAU;GAEjE,QAAO,MADc,KAAK,aAAa,UAAU,EAAA,CACnC,MAAM;EACtB,SAAS,KAAK;GACZ,OAAO,MAAM,EAAE,IAAI,GAAG,qBAAqB;GAC3C,OAAO;EACT;CACF;;;;;;;CAQA,MAAc,qBACZ,aACmB;EAEnB,QAAO,MADa,YAAY,iBAAiB,KAAK,OAAO,OAAO,EAAA,CACvD,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,OAAO,gBAAgB;CACzD;CAEA,MAAM,aAAa,aAAwC;EACzD,OAAO,MAAM,gBAAgB;EAC7B,IAAI;GACF,MAAM,cAAc,MAAMA,oBAA6B;GAMvD,IAAI,OAAO;;;wCAGuB,KAAK,OAAO,QAAQ;;GAGtD,IAAI,aAAa;IACf,MAAM,eAAe,YAAY,QAAQ,MAAM,IAAI;IACnD,QAAQ,0BAA0B,aAAa;GACjD;GAEA,MAAM,SAAS,MAAM,YAAY,YAAY,EAAE,OAAO,KAAK,CAAC;GAE5D,IAAI,CAAC,OAAO,WAAW,QAAQ;IAC7B,OAAO,MAAM,oCAAoC;IACjD,OAAO,CAAC;GACV;GAEA,MAAM,cAAc,OAAO,UAAU,KAAK,OAAiB,GAAG,EAAG;GACjE,MAAM,YAAY,MAAM,YAAY,aAAa,aAAa;IAC5D;IACA;IACA;IACA;IACA;IACA;GACF,CAAC;GAED,MAAM,EAAE,gBAAgB,MAAM,KAAK,eAAe,SAAS;GAE3D,OAAO,UAAU,KAAK,QAAkB;IACtC,QAAQ,GAAG;IACX,OAAO,GAAG,OAAQ;IAClB,OAAO,YAAY,IAAI,GAAG,OAAQ,eAAe,IAAI,WAAW;IAChE,MAAM,GAAG,OAAQ;IACjB,WAAW,GAAG,OAAQ;IACtB,cAAc,GAAG,OAAQ;GAC3B,EAAE;EACJ,SAAS,KAAK;GACZ,OAAO,KAAK,EAAE,IAAI,GAAG,2BAA2B;GAChD,OAAO,CAAC;EACV;CACF;CAEA,MAAM,mBAAmB,OAA8B;EACrD,OAAO,MAAM,sBAAsB,MAAM,EAAE;EAC3C,IAAI;GACF,MAAM,QAAQ,MAAM,KAAK,UAAU,KAAK;GACxC,IAAI,OAAO,UAAU,UAAU,MAAM,QAAQ;IAC3C,MAAM,cAAc,MAAMA,oBAA6B;IACvD,MAAM,EAAE,WAAW,MAAM,KAAK,eAAe,SAAS;IACtD,MAAM,YAAY,eAChB,KAAA,GACA,CAAC;KAAE,IAAI;KAAW,MAAM;KAAwB,OAAO;IAAO,CAAC,GAC/D,MAAM,QACN,KAAK,OAAO,OACd;IACA,OAAO,MAAM,iBAAiB,MAAM,OAAO,IAAI,OAAO;GACxD;EACF,SAAS,KAAK;GACZ,OAAO,MAAM,EAAE,IAAI,GAAG,qBAAqB;EAC7C;CACF;CAEA,MAAM,YAAY,EAChB,OACA,MACA,OAAO,OACP,eAAe,QACwC;EACvD,OAAO,MAAM,eAAe;EAE5B,IAAI;GACF,MAAM,cAAc,MAAMA,oBAA6B;GACvD,MAAM,aAAa,iBAAiB,OAAO,KAAK,OAAO,UAAU;GACjE,MAAM,SAAS,MAAM,KAAK,aAAa,UAAU;GACjD,MAAM,EAAE,MAAM,WAAW,MAAM,KAAK,eAAe,SAAS;GAC5D,MAAM,SAAS,MAAM,KAAK,OAAO,SAAS;GAG1C,MAAM,aAAa,OAAO,QAAQ,UAAU,MAAM,UAAU,MAAM;GAClE,IAAI,WAAW,SAAS,GACtB,KAAK,IAAI,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;IAC1C,MAAM,cAAc,WAAW,EAAE,CAAC;IAClC,IAAI,aAAa;KACf,MAAM,YAAY,eAChB,KAAA,GACA,CACE;MACE,IAAI;MACJ,MAAM;MACN,OAAO;KACT,CACF,GACA,aACA,KAAK,OAAO,OACd;KACA,OAAO,KAAK,2BAA2B,aAAa;IACtD;GACF;GAGF,MAAM,gBACJ,WAAW,MAAM,OAAO,MAAM,UAAU,MAAM,UAAU,QAAQ;GAElE,IAAI,eAAe;IACjB,IAAI,cAAc,UAAU,YAAY,MAAM;KAC5C,OAAO,MAAM,wCAAwC;KACrD,OAAO;IACT;IACA,IAAI,cAAc,UAAU,YAAY,cAAc;KAEpD,IAAI,CAAC,cAAc,QAAQ;MACzB,OAAO,KAAK,oCAAoC;MAChD,OAAO;KACT;KACA,MAAM,YAAY,eAChB,KAAA,GACA;MACE;OAAE,IAAI;OAAW,MAAM;OAAwB,OAAO;MAAK;MAC3D;OACE,IAAI;OACJ,MAAM;OACN,OAAO;MACT;MACA;OACE,IAAI;OACJ,MAAM;OACN,OAAO,SAAS,IAAI;MACtB;MACA,GAAI,SAAS,CAAC,iBAAiB,SAAS,CAAC,IAAI,CAAC;KAChD,GACA,cAAc,QACd,KAAK,OAAO,OACd;KACA,OAAO,MAAM,mBAAmB,cAAc,QAAQ;KACtD,OAAO;IACT;IACA,IAAI,cAAc,UAAU,QAAQ;KAElC,IACE,cAAc,UAAU,cACxB,cAAc,SAAS,MACvB;MACA,IAAI,CAAC,cAAc,QAAQ;OACzB,OAAO,KAAK,oCAAoC;OAChD,OAAO;MACT;MACA,MAAM,YAAY,eAChB,KAAA,GACA;OACE;QACE,IAAI;QACJ,MAAM;QACN,OAAO;OACT;OACA;QACE,IAAI;QACJ,MAAM;QACN,OAAO,SAAS,IAAI;OACtB;OACA,GAAI,SAAS,CAAC,iBAAiB,SAAS,CAAC,IAAI,CAAC;MAChD,GACA,cAAc,QACd,KAAK,OAAO,OACd;MACA,OAAO,MAAM,kBAAkB,cAAc,QAAQ;MACrD,OAAO;KACT;KACA,OAAO,MAAM,UAAU,cAAc,OAAO,uBAAuB;KACnE,OAAO;IACT;GACF;GAYA,MAAM,iBAAiB,MAAM,KAAK,qBAAqB,WAAW;GAClE,IAAI,CAAC,eAAe,SAAS,KAAK,OAAO,YAAY,GAAG;IACtD,OAAO,KACL;KACE,cAAc,KAAK,OAAO;KAC1B;KACA,SAAS,KAAK,OAAO;KACrB,kBAAkB,GAAG,aAAa,IAAI,cAAc,CAAC,CAAC,cAAc;IACtE,GACA,gQACF;IACA,OAAO;GACT;GAEA,MAAM,cAAc,MAAM,YAAY,eACpC,KAAA,GACA;IACE;KACE,IAAI;KACJ,MAAM;KACN,OAAO,KAAK,OAAO;IACrB;IACA;KAAE,IAAI;KAAO,MAAM;KAAwB,OAAO;IAAW;IAC7D;KACE,IAAI;KACJ,MAAM;KACN,OAAO,SAAS,IAAI;IACtB;IACA,GAAI,SAAS,CAAC,iBAAiB,KAAK,CAAC,IAAI,CAAC;GAC5C,GACA,KAAK,OAAO,SACZ,KAAK,OAAO,YACd;GAQA,IAAI,CAAC,aAAa,IAAI;IACpB,OAAO,KACL,8DACF;IACA,OAAO;GACT;GAEA,OAAO,MAAM,sBAAsB,YAAY,IAAI;GACnD,OAAO;EACT,SAAS,KAAK;GACZ,OAAO,KAAK,EAAE,IAAI,GAAG,sBAAsB;GAC3C,OAAO;EACT;CACF;AACF"}
@@ -186,13 +186,6 @@ async function statCacheFile(pathName) {
186
186
  return null;
187
187
  }
188
188
  }
189
- function listCacheDir(path, options = { recursive: false }) {
190
- const fullPath = ensureCachePath(path);
191
- return fs.readdir(fullPath, {
192
- encoding: "utf-8",
193
- recursive: options.recursive
194
- });
195
- }
196
189
  async function rmCache(path) {
197
190
  const fullPath = ensureCachePath(path);
198
191
  await fs.rm(fullPath, {
@@ -208,14 +201,6 @@ async function cachePathExists(pathName) {
208
201
  return false;
209
202
  }
210
203
  }
211
- async function cachePathIsFile(pathName) {
212
- const path = ensureCachePath(pathName);
213
- try {
214
- return (await fs.stat(path)).isFile();
215
- } catch {
216
- return false;
217
- }
218
- }
219
204
  function readCacheFile(fileName, encoding) {
220
205
  const fullPath = ensureCachePath(fileName);
221
206
  return encoding ? fs.readFile(fullPath, encoding) : fs.readFile(fullPath);
@@ -236,6 +221,6 @@ async function getLocalFiles(fileNames) {
236
221
  return fileContentMap;
237
222
  }
238
223
  //#endregion
239
- export { cachePathExists, cachePathIsFile, chmodLocalFile, createCacheReadStream, createCacheWriteStream, deleteLocalFile, ensureCacheDir, ensureDir, ensureLocalDir, findLocalSiblingOrParent, findUpLocal, getLocalFiles, getParentDir, getSiblingFileName, isValidLocalPath, listCacheDir, localPathExists, localPathIsFile, localPathIsSymbolicLink, outputCacheFile, pipeline, privateCacheDir, readCacheFile, readLocalDirectory, readLocalFile, readLocalSymlink, readSystemFile, renameCacheFile, renameLocalFile, rmCache, statCacheFile, statLocalFile, writeLocalFile, writeSystemFile };
224
+ export { cachePathExists, chmodLocalFile, createCacheReadStream, createCacheWriteStream, deleteLocalFile, ensureCacheDir, ensureDir, ensureLocalDir, findLocalSiblingOrParent, findUpLocal, getLocalFiles, getParentDir, getSiblingFileName, isValidLocalPath, localPathExists, localPathIsFile, localPathIsSymbolicLink, outputCacheFile, pipeline, privateCacheDir, readCacheFile, readLocalDirectory, readLocalFile, readLocalSymlink, readSystemFile, renameCacheFile, renameLocalFile, rmCache, statCacheFile, statLocalFile, writeLocalFile, writeSystemFile };
240
225
 
241
226
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../lib/util/fs/index.ts"],"sourcesContent":["import stream from 'node:stream';\nimport util from 'node:util';\nimport { isNonEmptyString } from '@sindresorhus/is';\nimport { findUp } from 'find-up';\nimport fs from 'fs-extra';\nimport upath from 'upath';\nimport { GlobalConfig } from '../../config/global.ts';\nimport { logger } from '../../logger/index.ts';\nimport { logWarningIfUnicodeHiddenCharactersInPackageFile } from '../unicode.ts';\nimport { ensureCachePath, ensureLocalPath, isValidPath } from './util.ts';\n\nexport const pipeline = util.promisify(stream.pipeline);\n\nexport function getParentDir(fileName: string): string {\n return upath.parse(fileName).dir;\n}\n\nexport function getSiblingFileName(\n fileName: string,\n siblingName: string,\n): string {\n const subDirectory = getParentDir(fileName);\n return upath.join(subDirectory, siblingName);\n}\n\nexport async function readLocalFile(fileName: string): Promise<Buffer | null>;\nexport async function readLocalFile(\n fileName: string,\n encoding: 'utf8',\n): Promise<string | null>;\nexport async function readLocalFile(\n fileName: string,\n encoding?: BufferEncoding,\n): Promise<string | Buffer | null> {\n const localFileName = ensureLocalPath(fileName);\n try {\n const fileContent = encoding\n ? await fs.readFile(localFileName, encoding)\n : await fs.readFile(localFileName);\n\n logWarningIfUnicodeHiddenCharactersInPackageFile(fileName, fileContent);\n\n return fileContent;\n } catch (err) {\n logger.trace({ err }, 'Error reading local file');\n return null;\n }\n}\n\nexport async function readLocalSymlink(\n fileName: string,\n): Promise<string | null> {\n const localFileName = ensureLocalPath(fileName);\n try {\n const linkContent = await fs.readlink(localFileName);\n return linkContent;\n } catch (err) {\n logger.trace({ err }, 'Error reading local symlink');\n return null;\n }\n}\n\nexport async function writeLocalFile(\n fileName: string,\n fileContent: string | Buffer,\n): Promise<void> {\n const localFileName = ensureLocalPath(fileName);\n await fs.outputFile(localFileName, fileContent);\n}\n\nexport async function deleteLocalFile(fileName: string): Promise<void> {\n // This a failsafe and hopefully will never be triggered\n if (GlobalConfig.get('platform') === 'local') {\n throw new Error('Cannot delete file when platform=local');\n }\n const localDir = GlobalConfig.get('localDir');\n if (localDir) {\n const localFileName = ensureLocalPath(fileName);\n await fs.remove(localFileName);\n }\n}\n\nexport async function renameLocalFile(\n fromFile: string,\n toFile: string,\n): Promise<void> {\n const fromPath = ensureLocalPath(fromFile);\n const toPath = ensureLocalPath(toFile);\n await fs.move(fromPath, toPath);\n}\n\nexport async function renameCacheFile(\n fromFile: string,\n toFile: string,\n): Promise<void> {\n const fromPath = ensureCachePath(fromFile);\n const toPath = ensureCachePath(toFile);\n await fs.rename(fromPath, toPath);\n}\n\nexport async function ensureDir(dirName: string): Promise<void> {\n // v8 ignore else -- TODO: add test #40625\n if (isNonEmptyString(dirName)) {\n await fs.ensureDir(dirName);\n }\n}\n\nexport async function ensureLocalDir(dirName: string): Promise<string> {\n const fullPath = ensureLocalPath(dirName);\n await fs.ensureDir(fullPath);\n return fullPath;\n}\n\nexport async function ensureCacheDir(name: string): Promise<string> {\n const cacheDirName = ensureCachePath(`others/${name}`);\n await fs.ensureDir(cacheDirName);\n return cacheDirName;\n}\n\n/**\n * Return the path of the private cache directory. This directory is wiped\n * between repositories, so they can be used to store private registries' index\n * without risk of that information leaking to other repositories/users.\n */\nexport function privateCacheDir(): string {\n // TODO: types (#22198)\n const cacheDir = GlobalConfig.get('cacheDir');\n return upath.join(cacheDir, '__renovate-private-cache');\n}\n\nexport async function localPathExists(pathName: string): Promise<boolean> {\n // Works for both files as well as directories\n const path = ensureLocalPath(pathName);\n try {\n const s = await fs.stat(path);\n return !!s;\n } catch {\n return false;\n }\n}\n\n/**\n * Validate local path without throwing.\n * @param path Path to check\n * @returns `true` if given `path` is a valid local path, otherwise `false`.\n */\nexport function isValidLocalPath(path: string): boolean {\n return isValidPath(path, 'localDir');\n}\n\n/**\n * Tries to find `otherFileName` in the directory where\n * `existingFileNameWithPath` is, then in its parent directory, then in the\n * grandparent, until we reach the top-level directory. All paths\n * must be relative to `localDir`.\n */\nexport async function findLocalSiblingOrParent(\n existingFileNameWithPath: string,\n otherFileName: string,\n): Promise<string | null> {\n if (upath.isAbsolute(existingFileNameWithPath)) {\n return null;\n }\n if (upath.isAbsolute(otherFileName)) {\n return null;\n }\n\n let current = existingFileNameWithPath;\n while (current !== '') {\n current = getParentDir(current);\n const candidate = upath.join(current, otherFileName);\n if (await localPathExists(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n/**\n * Get files by name from directory\n */\nexport async function readLocalDirectory(path: string): Promise<string[]> {\n const localPath = ensureLocalPath(path);\n const fileList = await fs.readdir(localPath);\n return fileList;\n}\n\nexport function createCacheWriteStream(path: string): fs.WriteStream {\n const fullPath = ensureCachePath(path);\n return fs.createWriteStream(fullPath);\n}\n\nexport function createCacheReadStream(path: string): fs.ReadStream {\n const fullPath = ensureCachePath(path);\n return fs.createReadStream(fullPath);\n}\n\nexport async function localPathIsFile(pathName: string): Promise<boolean> {\n const path = ensureLocalPath(pathName);\n try {\n const s = await fs.stat(path);\n return s.isFile();\n } catch {\n return false;\n }\n}\n\nexport async function localPathIsSymbolicLink(\n pathName: string,\n): Promise<boolean> {\n const path = ensureLocalPath(pathName);\n try {\n const s = await fs.lstat(path);\n return s.isSymbolicLink();\n } catch {\n return false;\n }\n}\n\n/**\n * Find a file or directory by walking up parent directories within localDir\n */\n\nexport async function findUpLocal(\n fileName: string | string[],\n cwd: string,\n): Promise<string | null> {\n // TODO: types (#22198)\n const localDir = GlobalConfig.get('localDir');\n const absoluteCwd = upath.join(localDir, cwd);\n const normalizedAbsoluteCwd = upath.normalizeSafe(absoluteCwd);\n const res = await findUp(fileName, {\n cwd: normalizedAbsoluteCwd,\n type: 'file',\n });\n // Return null if nothing found\n if (!isNonEmptyString(res) || !isNonEmptyString(localDir)) {\n return null;\n }\n const safePath = upath.normalizeSafe(res);\n // Return relative path if file is inside of local dir\n if (safePath.startsWith(localDir)) {\n let relativePath = safePath.replace(localDir, '');\n // v8 ignore else -- TODO: add test #40625\n if (relativePath.startsWith('/')) {\n relativePath = relativePath.substring(1);\n }\n return relativePath;\n }\n // Return null if found file is outside of localDir\n return null;\n}\n\nexport function chmodLocalFile(\n fileName: string,\n mode: string | number,\n): Promise<void> {\n const fullFileName = ensureLocalPath(fileName);\n return fs.chmod(fullFileName, mode);\n}\n\nexport async function statLocalFile(\n fileName: string,\n): Promise<fs.Stats | null> {\n const fullFileName = ensureLocalPath(fileName);\n try {\n return await fs.stat(fullFileName);\n } catch {\n return null;\n }\n}\n\nexport async function statCacheFile(\n pathName: string,\n): Promise<fs.Stats | null> {\n const path = ensureCachePath(pathName);\n try {\n return await fs.stat(path);\n } catch {\n return null;\n }\n}\n\nexport function listCacheDir(\n path: string,\n options: { recursive: boolean } = { recursive: false },\n): Promise<string[]> {\n const fullPath = ensureCachePath(path);\n return fs.readdir(fullPath, {\n encoding: 'utf-8',\n recursive: options.recursive,\n });\n}\n\nexport async function rmCache(path: string): Promise<void> {\n const fullPath = ensureCachePath(path);\n // force: a missing path is a no-op, callers use this for cleanup\n await fs.rm(fullPath, { recursive: true, force: true });\n}\n\nexport async function cachePathExists(pathName: string): Promise<boolean> {\n const path = ensureCachePath(pathName);\n try {\n const s = await fs.stat(path);\n return !!s;\n } catch {\n return false;\n }\n}\n\nexport async function cachePathIsFile(pathName: string): Promise<boolean> {\n const path = ensureCachePath(pathName);\n try {\n const s = await fs.stat(path);\n return s.isFile();\n } catch {\n return false;\n }\n}\n\nexport async function readCacheFile(fileName: string): Promise<Buffer>;\nexport async function readCacheFile(\n fileName: string,\n encoding: 'utf8',\n): Promise<string>;\nexport function readCacheFile(\n fileName: string,\n encoding?: BufferEncoding,\n): Promise<string | Buffer> {\n const fullPath = ensureCachePath(fileName);\n return encoding ? fs.readFile(fullPath, encoding) : fs.readFile(fullPath);\n}\n\nexport function outputCacheFile(\n file: string,\n data: string | NodeJS.ArrayBufferView,\n): Promise<void> {\n const filePath = ensureCachePath(file);\n return fs.outputFile(filePath, data);\n}\n\nexport async function readSystemFile(fileName: string): Promise<Buffer>;\nexport async function readSystemFile(\n fileName: string,\n encoding: 'utf8',\n): Promise<string>;\nexport function readSystemFile(\n fileName: string,\n encoding?: BufferEncoding,\n): Promise<string | Buffer> {\n return encoding ? fs.readFile(fileName, encoding) : fs.readFile(fileName);\n}\n\nexport async function writeSystemFile(\n fileName: string,\n data: string | Buffer,\n): Promise<void> {\n await fs.outputFile(fileName, data);\n}\n\nexport async function getLocalFiles(\n fileNames: string[],\n): Promise<Record<string, string | null>> {\n const fileContentMap: Record<string, string | null> = {};\n\n for (const fileName of fileNames) {\n fileContentMap[fileName] = await readLocalFile(fileName, 'utf8');\n }\n\n return fileContentMap;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../lib/util/fs/index.ts"],"sourcesContent":["import stream from 'node:stream';\nimport util from 'node:util';\nimport { isNonEmptyString } from '@sindresorhus/is';\nimport { findUp } from 'find-up';\nimport fs from 'fs-extra';\nimport upath from 'upath';\nimport { GlobalConfig } from '../../config/global.ts';\nimport { logger } from '../../logger/index.ts';\nimport { logWarningIfUnicodeHiddenCharactersInPackageFile } from '../unicode.ts';\nimport { ensureCachePath, ensureLocalPath, isValidPath } from './util.ts';\n\nexport const pipeline = util.promisify(stream.pipeline);\n\nexport function getParentDir(fileName: string): string {\n return upath.parse(fileName).dir;\n}\n\nexport function getSiblingFileName(\n fileName: string,\n siblingName: string,\n): string {\n const subDirectory = getParentDir(fileName);\n return upath.join(subDirectory, siblingName);\n}\n\nexport async function readLocalFile(fileName: string): Promise<Buffer | null>;\nexport async function readLocalFile(\n fileName: string,\n encoding: 'utf8',\n): Promise<string | null>;\nexport async function readLocalFile(\n fileName: string,\n encoding?: BufferEncoding,\n): Promise<string | Buffer | null> {\n const localFileName = ensureLocalPath(fileName);\n try {\n const fileContent = encoding\n ? await fs.readFile(localFileName, encoding)\n : await fs.readFile(localFileName);\n\n logWarningIfUnicodeHiddenCharactersInPackageFile(fileName, fileContent);\n\n return fileContent;\n } catch (err) {\n logger.trace({ err }, 'Error reading local file');\n return null;\n }\n}\n\nexport async function readLocalSymlink(\n fileName: string,\n): Promise<string | null> {\n const localFileName = ensureLocalPath(fileName);\n try {\n const linkContent = await fs.readlink(localFileName);\n return linkContent;\n } catch (err) {\n logger.trace({ err }, 'Error reading local symlink');\n return null;\n }\n}\n\nexport async function writeLocalFile(\n fileName: string,\n fileContent: string | Buffer,\n): Promise<void> {\n const localFileName = ensureLocalPath(fileName);\n await fs.outputFile(localFileName, fileContent);\n}\n\nexport async function deleteLocalFile(fileName: string): Promise<void> {\n // This a failsafe and hopefully will never be triggered\n if (GlobalConfig.get('platform') === 'local') {\n throw new Error('Cannot delete file when platform=local');\n }\n const localDir = GlobalConfig.get('localDir');\n if (localDir) {\n const localFileName = ensureLocalPath(fileName);\n await fs.remove(localFileName);\n }\n}\n\nexport async function renameLocalFile(\n fromFile: string,\n toFile: string,\n): Promise<void> {\n const fromPath = ensureLocalPath(fromFile);\n const toPath = ensureLocalPath(toFile);\n await fs.move(fromPath, toPath);\n}\n\nexport async function renameCacheFile(\n fromFile: string,\n toFile: string,\n): Promise<void> {\n const fromPath = ensureCachePath(fromFile);\n const toPath = ensureCachePath(toFile);\n await fs.rename(fromPath, toPath);\n}\n\nexport async function ensureDir(dirName: string): Promise<void> {\n // v8 ignore else -- TODO: add test #40625\n if (isNonEmptyString(dirName)) {\n await fs.ensureDir(dirName);\n }\n}\n\nexport async function ensureLocalDir(dirName: string): Promise<string> {\n const fullPath = ensureLocalPath(dirName);\n await fs.ensureDir(fullPath);\n return fullPath;\n}\n\nexport async function ensureCacheDir(name: string): Promise<string> {\n const cacheDirName = ensureCachePath(`others/${name}`);\n await fs.ensureDir(cacheDirName);\n return cacheDirName;\n}\n\n/**\n * Return the path of the private cache directory. This directory is wiped\n * between repositories, so they can be used to store private registries' index\n * without risk of that information leaking to other repositories/users.\n */\nexport function privateCacheDir(): string {\n // TODO: types (#22198)\n const cacheDir = GlobalConfig.get('cacheDir');\n return upath.join(cacheDir, '__renovate-private-cache');\n}\n\nexport async function localPathExists(pathName: string): Promise<boolean> {\n // Works for both files as well as directories\n const path = ensureLocalPath(pathName);\n try {\n const s = await fs.stat(path);\n return !!s;\n } catch {\n return false;\n }\n}\n\n/**\n * Validate local path without throwing.\n * @param path Path to check\n * @returns `true` if given `path` is a valid local path, otherwise `false`.\n */\nexport function isValidLocalPath(path: string): boolean {\n return isValidPath(path, 'localDir');\n}\n\n/**\n * Tries to find `otherFileName` in the directory where\n * `existingFileNameWithPath` is, then in its parent directory, then in the\n * grandparent, until we reach the top-level directory. All paths\n * must be relative to `localDir`.\n */\nexport async function findLocalSiblingOrParent(\n existingFileNameWithPath: string,\n otherFileName: string,\n): Promise<string | null> {\n if (upath.isAbsolute(existingFileNameWithPath)) {\n return null;\n }\n if (upath.isAbsolute(otherFileName)) {\n return null;\n }\n\n let current = existingFileNameWithPath;\n while (current !== '') {\n current = getParentDir(current);\n const candidate = upath.join(current, otherFileName);\n if (await localPathExists(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n/**\n * Get files by name from directory\n */\nexport async function readLocalDirectory(path: string): Promise<string[]> {\n const localPath = ensureLocalPath(path);\n const fileList = await fs.readdir(localPath);\n return fileList;\n}\n\nexport function createCacheWriteStream(path: string): fs.WriteStream {\n const fullPath = ensureCachePath(path);\n return fs.createWriteStream(fullPath);\n}\n\nexport function createCacheReadStream(path: string): fs.ReadStream {\n const fullPath = ensureCachePath(path);\n return fs.createReadStream(fullPath);\n}\n\nexport async function localPathIsFile(pathName: string): Promise<boolean> {\n const path = ensureLocalPath(pathName);\n try {\n const s = await fs.stat(path);\n return s.isFile();\n } catch {\n return false;\n }\n}\n\nexport async function localPathIsSymbolicLink(\n pathName: string,\n): Promise<boolean> {\n const path = ensureLocalPath(pathName);\n try {\n const s = await fs.lstat(path);\n return s.isSymbolicLink();\n } catch {\n return false;\n }\n}\n\n/**\n * Find a file or directory by walking up parent directories within localDir\n */\n\nexport async function findUpLocal(\n fileName: string | string[],\n cwd: string,\n): Promise<string | null> {\n // TODO: types (#22198)\n const localDir = GlobalConfig.get('localDir');\n const absoluteCwd = upath.join(localDir, cwd);\n const normalizedAbsoluteCwd = upath.normalizeSafe(absoluteCwd);\n const res = await findUp(fileName, {\n cwd: normalizedAbsoluteCwd,\n type: 'file',\n });\n // Return null if nothing found\n if (!isNonEmptyString(res) || !isNonEmptyString(localDir)) {\n return null;\n }\n const safePath = upath.normalizeSafe(res);\n // Return relative path if file is inside of local dir\n if (safePath.startsWith(localDir)) {\n let relativePath = safePath.replace(localDir, '');\n // v8 ignore else -- TODO: add test #40625\n if (relativePath.startsWith('/')) {\n relativePath = relativePath.substring(1);\n }\n return relativePath;\n }\n // Return null if found file is outside of localDir\n return null;\n}\n\nexport function chmodLocalFile(\n fileName: string,\n mode: string | number,\n): Promise<void> {\n const fullFileName = ensureLocalPath(fileName);\n return fs.chmod(fullFileName, mode);\n}\n\nexport async function statLocalFile(\n fileName: string,\n): Promise<fs.Stats | null> {\n const fullFileName = ensureLocalPath(fileName);\n try {\n return await fs.stat(fullFileName);\n } catch {\n return null;\n }\n}\n\nexport async function statCacheFile(\n pathName: string,\n): Promise<fs.Stats | null> {\n const path = ensureCachePath(pathName);\n try {\n return await fs.stat(path);\n } catch {\n return null;\n }\n}\n\nexport function listCacheDir(\n path: string,\n options: { recursive: boolean } = { recursive: false },\n): Promise<string[]> {\n const fullPath = ensureCachePath(path);\n return fs.readdir(fullPath, {\n encoding: 'utf-8',\n recursive: options.recursive,\n });\n}\n\nexport async function rmCache(path: string): Promise<void> {\n const fullPath = ensureCachePath(path);\n // force: a missing path is a no-op, callers use this for cleanup\n await fs.rm(fullPath, { recursive: true, force: true });\n}\n\nexport async function cachePathExists(pathName: string): Promise<boolean> {\n const path = ensureCachePath(pathName);\n try {\n const s = await fs.stat(path);\n return !!s;\n } catch {\n return false;\n }\n}\n\nexport async function cachePathIsFile(pathName: string): Promise<boolean> {\n const path = ensureCachePath(pathName);\n try {\n const s = await fs.stat(path);\n return s.isFile();\n } catch {\n return false;\n }\n}\n\nexport async function readCacheFile(fileName: string): Promise<Buffer>;\nexport async function readCacheFile(\n fileName: string,\n encoding: 'utf8',\n): Promise<string>;\nexport function readCacheFile(\n fileName: string,\n encoding?: BufferEncoding,\n): Promise<string | Buffer> {\n const fullPath = ensureCachePath(fileName);\n return encoding ? fs.readFile(fullPath, encoding) : fs.readFile(fullPath);\n}\n\nexport function outputCacheFile(\n file: string,\n data: string | NodeJS.ArrayBufferView,\n): Promise<void> {\n const filePath = ensureCachePath(file);\n return fs.outputFile(filePath, data);\n}\n\nexport async function readSystemFile(fileName: string): Promise<Buffer>;\nexport async function readSystemFile(\n fileName: string,\n encoding: 'utf8',\n): Promise<string>;\nexport function readSystemFile(\n fileName: string,\n encoding?: BufferEncoding,\n): Promise<string | Buffer> {\n return encoding ? fs.readFile(fileName, encoding) : fs.readFile(fileName);\n}\n\nexport async function writeSystemFile(\n fileName: string,\n data: string | Buffer,\n): Promise<void> {\n await fs.outputFile(fileName, data);\n}\n\nexport async function getLocalFiles(\n fileNames: string[],\n): Promise<Record<string, string | null>> {\n const fileContentMap: Record<string, string | null> = {};\n\n for (const fileName of fileNames) {\n fileContentMap[fileName] = await readLocalFile(fileName, 'utf8');\n }\n\n return fileContentMap;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "renovate",
3
3
  "description": "Automated dependency updates. Flexible so you don't need to be.",
4
- "version": "44.25.0",
4
+ "version": "44.26.3",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "renovate": "dist/renovate.js",
@@ -204,6 +204,7 @@
204
204
  "validate-npm-package-name": "7.0.2",
205
205
  "xmldoc": "2.0.3",
206
206
  "yaml": "2.9.0",
207
+ "yauzl": "3.4.0",
207
208
  "zod": "4.4.3"
208
209
  },
209
210
  "optionalDependencies": {
@@ -254,6 +255,8 @@
254
255
  "@types/semver-utils": "1.1.3",
255
256
  "@types/tmp": "0.2.6",
256
257
  "@types/validate-npm-package-name": "4.0.2",
258
+ "@types/yauzl": "3.4.0",
259
+ "@types/yazl": "3.3.1",
257
260
  "@vitest/coverage-v8": "4.1.10",
258
261
  "ajv": "8.20.0",
259
262
  "ajv-formats": "3.0.1",
@@ -286,7 +289,8 @@
286
289
  "unified": "11.0.5",
287
290
  "vite": "8.2.0",
288
291
  "vitest": "4.1.10",
289
- "vitest-mock-extended": "5.1.1"
292
+ "vitest-mock-extended": "5.1.1",
293
+ "yazl": "3.3.1"
290
294
  },
291
295
  "files": [
292
296
  "dist",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$id": "https://docs.renovatebot.com/renovate-schema.json",
3
- "title": "JSON schema for Renovate 44.25.0 config files (https://renovatebot.com/)",
3
+ "title": "JSON schema for Renovate 44.26.3 config files (https://renovatebot.com/)",
4
4
  "$schema": "http://json-schema.org/draft-07/schema#",
5
- "x-renovate-version": "44.25.0",
5
+ "x-renovate-version": "44.26.3",
6
6
  "allowComments": true,
7
7
  "type": "object",
8
8
  "definitions": {