Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export {
formatSizeChange,
sanitizeCacheKey,
convertGitUrlToHttps,
normalizeCatalogFile,
} from './utils/formatters';

export {
Expand Down
23 changes: 23 additions & 0 deletions src/core/utils/formatters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
convertGitUrlToHttps,
formatBytes,
formatSizeChange,
normalizeCatalogFile,
} from './formatters';

describe('sanitizeCacheKey', () => {
Expand All @@ -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');
Expand Down
22 changes: 22 additions & 0 deletions src/core/utils/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
55 changes: 55 additions & 0 deletions src/github-issues/issueDescriptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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',
Expand Down
7 changes: 5 additions & 2 deletions src/github-issues/issueDescriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
findFirstVersionOfType,
formatBytes,
formatSizeChange,
normalizeCatalogFile,
resolveUrlPatterns,
} from '../core/index';
import { helpers } from './templates/helpers';
Expand Down Expand Up @@ -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.';
Expand Down Expand Up @@ -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}\`.`;
Expand Down
2 changes: 1 addition & 1 deletion src/github-issues/templates/group-description.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/github-issues/templates/issue-description.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
54 changes: 54 additions & 0 deletions src/linear/issueDescriptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'] })],
Expand Down Expand Up @@ -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',
Expand Down
7 changes: 5 additions & 2 deletions src/linear/issueDescriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
findFirstVersionOfType,
formatBytes,
formatSizeChange,
normalizeCatalogFile,
resolveUrlPatterns,
} from '../core/index';
import { helpers } from './templates/helpers';
Expand Down Expand Up @@ -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.';
Expand Down Expand Up @@ -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}\`.`;
Expand Down
2 changes: 1 addition & 1 deletion src/linear/templates/group-description.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/linear/templates/issue-description.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
Loading