diff --git a/CHANGELOG.md b/CHANGELOG.md index 628b882..7d4697d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ ### Fixed +- Catalog-managed dependencies no longer produce a broken "How to Update" instruction in generated GitHub and Linear issues. + - Previously, when a provider had no catalog file path available, the instruction rendered nonsense: either ``Edit `undefined`:`` or an "Edit" line with empty backticks and no filename. + - Issues now fall back to generic wording ("managed in the catalog. Update it in the catalog:") when the catalog file is unknown, and still show the exact file (e.g. ``Edit `pnpm-workspace.yaml`:``) when it is known. + - Both single-dependency and grouped issues are covered. A new `normalizeCatalogFile` helper (exported from `@dependicus/core`) treats missing, empty, and the literal `"undefined"`/`"null"` strings as "no catalog file". - Fix version numbers without `.` failing to match open tickets, resulting in duplicates ### Removed diff --git a/src/core/index.ts b/src/core/index.ts index d9ed9a4..043c4e5 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -37,6 +37,7 @@ export { formatSizeChange, sanitizeCacheKey, convertGitUrlToHttps, + normalizeCatalogFile, } from './utils/formatters'; export { diff --git a/src/core/utils/formatters.test.ts b/src/core/utils/formatters.test.ts index 371c6d0..8ec1a05 100644 --- a/src/core/utils/formatters.test.ts +++ b/src/core/utils/formatters.test.ts @@ -8,6 +8,7 @@ import { convertGitUrlToHttps, formatBytes, formatSizeChange, + normalizeCatalogFile, } from './formatters'; describe('sanitizeCacheKey', () => { @@ -25,6 +26,28 @@ describe('sanitizeCacheKey', () => { }); }); +describe('normalizeCatalogFile', () => { + it('returns a real path unchanged (trimmed)', () => { + expect(normalizeCatalogFile('pnpm-workspace.yaml')).toBe('pnpm-workspace.yaml'); + expect(normalizeCatalogFile(' package.json ')).toBe('package.json'); + }); + + it('returns undefined for a missing path', () => { + expect(normalizeCatalogFile(undefined)).toBeUndefined(); + expect(normalizeCatalogFile(null as unknown as string)).toBeUndefined(); + }); + + it('returns undefined for empty or whitespace-only paths', () => { + expect(normalizeCatalogFile('')).toBeUndefined(); + expect(normalizeCatalogFile(' ')).toBeUndefined(); + }); + + it('treats the literal strings "undefined" and "null" as missing', () => { + expect(normalizeCatalogFile('undefined')).toBeUndefined(); + expect(normalizeCatalogFile('null')).toBeUndefined(); + }); +}); + describe('formatDate', () => { it('formats ISO date to YYYY-MM-DD', () => { expect(formatDate('2024-03-15T10:30:00.000Z')).toBe('2024-03-15'); diff --git a/src/core/utils/formatters.ts b/src/core/utils/formatters.ts index 82a9e7a..f7e80c7 100644 --- a/src/core/utils/formatters.ts +++ b/src/core/utils/formatters.ts @@ -8,6 +8,28 @@ export function sanitizeCacheKey(str: string): string { return str.replace(/[^a-zA-Z0-9._-]/g, '_'); } +/** + * Normalize a provider's catalog file path for display in issue templates. + * + * Returns `undefined` for values that would render as a broken instruction such + * as `Edit ``:` or `Edit `undefined`:`. That covers a genuinely missing path + * (`undefined`/`null`), an empty/whitespace-only string, and the literal strings + * `"undefined"`/`"null"` (which can leak in when an upstream value was + * string-interpolated before reaching the template). Callers should treat a + * `undefined` result as "no specific catalog file" and fall back to generic + * wording. + */ +export function normalizeCatalogFile(catalogFile: string | undefined): string | undefined { + if (catalogFile === undefined || catalogFile === null) { + return undefined; + } + const trimmed = catalogFile.trim(); + if (trimmed === '' || trimmed === 'undefined' || trimmed === 'null') { + return undefined; + } + return trimmed; +} + export function formatDate(isoDate: string | undefined): string | undefined { if (!isoDate) { return undefined; diff --git a/src/github-issues/issueDescriptions.test.ts b/src/github-issues/issueDescriptions.test.ts index 7897af8..83d5968 100644 --- a/src/github-issues/issueDescriptions.test.ts +++ b/src/github-issues/issueDescriptions.test.ts @@ -339,6 +339,38 @@ describe('buildIssueDescription', () => { expect(result).toContain('test-pkg: 2.0.0'); expect(result).not.toContain("'test-pkg'"); }); + + it('avoids a broken catalog file reference when catalogFile is missing', () => { + const dep = makeDependency(); + const store = makeStore(dep); + const { catalogFile: _catalogFile, ...providerInfoWithoutCatalogFile } = npmProviderInfo; + const result = buildIssueDescription({ + dep: dep, + store: store, + minVersion: '1.1.0', + effectiveLatestVersion: '2.0.0', + getDetailUrl: testGetDetailUrl, + providerInfo: providerInfoWithoutCatalogFile, + }); + expect(result).toContain('managed in the catalog'); + expect(result).not.toContain('Edit ``'); + expect(result).not.toContain('undefined'); + }); + + it('treats a literal "undefined" catalogFile as missing', () => { + const dep = makeDependency(); + const store = makeStore(dep); + const result = buildIssueDescription({ + dep: dep, + store: store, + minVersion: '1.1.0', + effectiveLatestVersion: '2.0.0', + getDetailUrl: testGetDetailUrl, + providerInfo: { ...npmProviderInfo, catalogFile: 'undefined' }, + }); + expect(result).toContain('managed in the catalog'); + expect(result).not.toContain('Edit `undefined`'); + }); }); describe('buildGroupIssueDescription', () => { @@ -429,6 +461,29 @@ describe('buildGroupIssueDescription', () => { expect(result).toContain('react: 2.0.0'); }); + it('avoids a broken catalog file reference in groups when catalogFile is missing', () => { + const group: OutdatedGroup = { + groupName: 'test-group', + dependencies: [makeDependency({ name: 'react' })], + owner: 'myorg', + repo: 'myrepo', + policy: { type: 'dueDate' }, + worstCompliance: { updateType: 'major', daysOverdue: 0, thresholdDays: 360 }, + }; + + const store = makeGroupStore(group); + const { catalogFile: _catalogFile, ...providerInfoWithoutCatalogFile } = npmProviderInfo; + const result = buildGroupIssueDescription({ + group: group, + store: store, + getDetailUrl: testGetDetailUrl, + providerInfoMap: new Map([['npm', providerInfoWithoutCatalogFile]]), + }); + expect(result).toContain('Update each dependency in the catalog'); + expect(result).not.toContain('`` catalog'); + expect(result).not.toContain('undefined'); + }); + it('quotes scoped package names in group catalog YAML', () => { const group: OutdatedGroup = { groupName: 'scoped-group', diff --git a/src/github-issues/issueDescriptions.ts b/src/github-issues/issueDescriptions.ts index b04c43f..d24c03b 100644 --- a/src/github-issues/issueDescriptions.ts +++ b/src/github-issues/issueDescriptions.ts @@ -6,6 +6,7 @@ import { findFirstVersionOfType, formatBytes, formatSizeChange, + normalizeCatalogFile, resolveUrlPatterns, } from '../core/index'; import { helpers } from './templates/helpers'; @@ -113,7 +114,8 @@ export function buildIssueDescription({ }; }); - const { installCommand, supportsCatalog, catalogFile } = providerInfo; + const { installCommand, supportsCatalog } = providerInfo; + const catalogFile = normalizeCatalogFile(providerInfo.catalogFile); const patchHint = providerInfo.patchHint ?? 'This dependency has local patches applied. When upgrading, check if the patches are still needed or should be removed.'; @@ -209,7 +211,8 @@ export function buildGroupIssueDescription({ `No provider info for ecosystem "${firstDep.ecosystem}" in group "${groupName}"`, ); } - const { installCommand, supportsCatalog, catalogFile } = groupProviderInfo; + const { installCommand, supportsCatalog } = groupProviderInfo; + const catalogFile = normalizeCatalogFile(groupProviderInfo.catalogFile); const updateInstructions = groupProviderInfo.updateInstructions ?? `Update each dependency's version in the appropriate config file, then run \`${installCommand}\`.`; diff --git a/src/github-issues/templates/group-description.hbs b/src/github-issues/templates/group-description.hbs index 0de9f23..97e8749 100644 --- a/src/github-issues/templates/group-description.hbs +++ b/src/github-issues/templates/group-description.hbs @@ -38,7 +38,7 @@ This issue tracks updates for the **{{groupName}}** dependency group. ## How to Update {{#if supportsCatalog}} -Update each dependency in the `{{catalogFile}}` catalog (if applicable) or individual package.json files: +Update each dependency in the {{#if catalogFile}}`{{catalogFile}}` catalog{{else}}catalog{{/if}} (if applicable) or individual package.json files: ```yaml catalog: diff --git a/src/github-issues/templates/issue-description.hbs b/src/github-issues/templates/issue-description.hbs index e98e0bd..b2d79f0 100644 --- a/src/github-issues/templates/issue-description.hbs +++ b/src/github-issues/templates/issue-description.hbs @@ -34,7 +34,7 @@ {{#if supportsCatalog}} {{#if inCatalog}} -This dependency is managed in the catalog. Edit `{{catalogFile}}`: +{{#if catalogFile}}This dependency is managed in the catalog. Edit `{{catalogFile}}`:{{else}}This dependency is managed in the catalog. Update it in the catalog:{{/if}} ```yaml catalog: @@ -46,7 +46,7 @@ Then, run `{{installCommand}}` to update the lockfile. {{#if shouldRecommendCatalog}} This dependency is used by {{usedByCount}} packages. Consider adding it to the catalog: -1. **Add to catalog** - Edit `{{catalogFile}}`: +1. **Add to catalog** - {{#if catalogFile}}Edit `{{catalogFile}}`:{{else}}Add it to the catalog:{{/if}} ```yaml catalog: diff --git a/src/linear/issueDescriptions.test.ts b/src/linear/issueDescriptions.test.ts index e7ed60c..2e8da29 100644 --- a/src/linear/issueDescriptions.test.ts +++ b/src/linear/issueDescriptions.test.ts @@ -296,6 +296,38 @@ describe('buildIssueDescription', () => { expect(result).toContain(' test-pkg: 2.0.0'); }); + it('avoids a broken catalog file reference when catalogFile is missing', () => { + const pkg = makeDependency(); + const store = makeStore(pkg); + const { catalogFile: _catalogFile, ...providerInfoWithoutCatalogFile } = npmProviderInfo; + const result = buildIssueDescription({ + dep: pkg, + store: store, + minVersion: '1.1.0', + effectiveLatestVersion: '2.0.0', + getDetailUrl: testGetDetailUrl, + providerInfo: providerInfoWithoutCatalogFile, + }); + expect(result).toContain('managed in the catalog'); + expect(result).not.toContain('Edit ``'); + expect(result).not.toContain('undefined'); + }); + + it('treats a literal "undefined" catalogFile as missing', () => { + const pkg = makeDependency(); + const store = makeStore(pkg); + const result = buildIssueDescription({ + dep: pkg, + store: store, + minVersion: '1.1.0', + effectiveLatestVersion: '2.0.0', + getDetailUrl: testGetDetailUrl, + providerInfo: { ...npmProviderInfo, catalogFile: 'undefined' }, + }); + expect(result).toContain('managed in the catalog'); + expect(result).not.toContain('Edit `undefined`'); + }); + it('shows non-catalog How to Update for single consumer', () => { const pkg = makeDependency({ versions: [makeVersion({ inCatalog: false, usedBy: ['@app/web'] })], @@ -702,6 +734,28 @@ describe('buildGroupIssueDescription', () => { expect(result).toContain('react: 2.0.0'); }); + it('avoids a broken catalog file reference in groups when catalogFile is missing', () => { + const group: OutdatedGroup = { + groupName: 'test-group', + dependencies: [makeDependency({ name: 'react' })], + teamId: 'team-123', + policy: { type: 'dueDate' }, + worstCompliance: { updateType: 'major', daysOverdue: 0, thresholdDays: 360 }, + }; + + const store = makeGroupStore(group); + const { catalogFile: _catalogFile, ...providerInfoWithoutCatalogFile } = npmProviderInfo; + const result = buildGroupIssueDescription({ + group: group, + store: store, + getDetailUrl: testGetDetailUrl, + providerInfoMap: new Map([['npm', providerInfoWithoutCatalogFile]]), + }); + expect(result).toContain('Update each dependency in the catalog'); + expect(result).not.toContain('`` catalog'); + expect(result).not.toContain('undefined'); + }); + it('quotes scoped package names in group catalog YAML', () => { const group: OutdatedGroup = { groupName: 'scoped-group', diff --git a/src/linear/issueDescriptions.ts b/src/linear/issueDescriptions.ts index ebcce6d..5b70938 100644 --- a/src/linear/issueDescriptions.ts +++ b/src/linear/issueDescriptions.ts @@ -6,6 +6,7 @@ import { findFirstVersionOfType, formatBytes, formatSizeChange, + normalizeCatalogFile, resolveUrlPatterns, } from '../core/index'; import { helpers } from './templates/helpers'; @@ -111,7 +112,8 @@ export function buildIssueDescription({ }; }); - const { installCommand, supportsCatalog, catalogFile } = providerInfo; + const { installCommand, supportsCatalog } = providerInfo; + const catalogFile = normalizeCatalogFile(providerInfo.catalogFile); const patchHint = providerInfo.patchHint ?? 'This dependency has local patches applied. When upgrading, check if the patches are still needed or should be removed.'; @@ -204,7 +206,8 @@ export function buildGroupIssueDescription({ `No provider info for ecosystem "${firstDep.ecosystem}" in group "${groupName}"`, ); } - const { installCommand, supportsCatalog, catalogFile } = groupProviderInfo; + const { installCommand, supportsCatalog } = groupProviderInfo; + const catalogFile = normalizeCatalogFile(groupProviderInfo.catalogFile); const updateInstructions = groupProviderInfo.updateInstructions ?? `Update each dependency's version in the appropriate config file, then run \`${installCommand}\`.`; diff --git a/src/linear/templates/group-description.hbs b/src/linear/templates/group-description.hbs index 012dfa8..4a661c7 100644 --- a/src/linear/templates/group-description.hbs +++ b/src/linear/templates/group-description.hbs @@ -35,7 +35,7 @@ This issue tracks updates for the **{{groupName}}** dependency group. ## How to Update {{#if supportsCatalog}} -Update each dependency in the `{{catalogFile}}` catalog (if applicable) or individual package.json files: +Update each dependency in the {{#if catalogFile}}`{{catalogFile}}` catalog{{else}}catalog{{/if}} (if applicable) or individual package.json files: ```yaml catalog: diff --git a/src/linear/templates/issue-description.hbs b/src/linear/templates/issue-description.hbs index 46e15e9..a15f582 100644 --- a/src/linear/templates/issue-description.hbs +++ b/src/linear/templates/issue-description.hbs @@ -31,7 +31,7 @@ {{#if supportsCatalog}} {{#if inCatalog}} -This dependency is managed in the catalog. Edit `{{catalogFile}}`: +{{#if catalogFile}}This dependency is managed in the catalog. Edit `{{catalogFile}}`:{{else}}This dependency is managed in the catalog. Update it in the catalog:{{/if}} ```yaml catalog: @@ -43,7 +43,7 @@ Then, run `{{installCommand}}` to update the lockfile. {{#if shouldRecommendCatalog}} This dependency is used by {{usedByCount}} packages. Consider adding it to the catalog: -1. **Add to catalog** - Edit `{{catalogFile}}`: +1. **Add to catalog** - {{#if catalogFile}}Edit `{{catalogFile}}`:{{else}}Add it to the catalog:{{/if}} ```yaml catalog: