diff --git a/CHANGELOG.md b/CHANGELOG.md index d9420f3..807be33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,9 @@ ### Fixed +- Issue descriptions get the provider's template text again. `update` wrote `catalogFile`, `patchHint` and the `update*` strings to `dependencies.json`. Reading the file back removed them. A Linear or GitHub issue then said to edit `undefined` to add a dependency to the catalog. +- Each clause of the "Why this matters" sentence starts with a capital letter. It read `4 Snyk advisories. fix available in a newer version.` +- The owner line in an issue's summary has a label, like the lines around it. - Links from a grouping page to a dependency's page work when the grouping spans ecosystems. Those pages sit at the site root, so the `../details/…` path a plugin builds leads nowhere, which broke `BasicCompliancePlugin`'s flagged entries. `GroupingFlag.detailLink` is optional now and Dependicus resolves it, and `GroupingDetailContext.detailLinkFor` resolves one for a link inside a section's own HTML. - The due date was wrong on an issue that covers two versions of one dependency. Say `react` 17 needs 17.0.5 with a 28-day threshold and `react` 18 needs 18.2.1 with a 10-day threshold. The issue asked for 18.2.1 but added the 28 days to its release date. It now uses the threshold that belongs to the version it asks for. diff --git a/src/core/schema.test.ts b/src/core/schema.test.ts index 28c0876..0737c7d 100644 --- a/src/core/schema.test.ts +++ b/src/core/schema.test.ts @@ -52,6 +52,32 @@ describe('schema', () => { expect(result.facts.dependency.react).toBeDefined(); }); + it('keeps the provider fields the issue templates read', () => { + const provider = { + ...validInput.providers[0], + installCommand: 'pnpm install', + catalogFile: 'pnpm-workspace.yaml', + updatePrefix: 'Update the version in:', + updateSuffix: 'Then run `pnpm install`.', + updateInstructions: 'Bump each package.', + patchHint: 'Check the patches in `patches/` still apply.', + }; + const result = parseDependicusOutput({ ...validInput, providers: [provider] }); + const { dependencies: _deps, ...kept } = result.providers[0]!; + expect(kept).toEqual({ + name: 'pnpm', + ecosystem: 'npm', + supportsCatalog: true, + installCommand: 'pnpm install', + urlPatterns: {}, + catalogFile: 'pnpm-workspace.yaml', + updatePrefix: 'Update the version in:', + updateSuffix: 'Then run `pnpm install`.', + updateInstructions: 'Bump each package.', + patchHint: 'Check the patches in `patches/` still apply.', + }); + }); + it('accepts input with empty facts', () => { const minimal = { metadata: { diff --git a/src/core/schema.ts b/src/core/schema.ts index 2d1645b..4146415 100644 --- a/src/core/schema.ts +++ b/src/core/schema.ts @@ -22,6 +22,11 @@ const providerOutputSchema = z.object({ supportsCatalog: z.boolean(), installCommand: z.string().default('install'), urlPatterns: z.record(z.string(), z.string()).default({}), + updatePrefix: z.string().optional(), + updateSuffix: z.string().optional(), + updateInstructions: z.string().optional(), + catalogFile: z.string().optional(), + patchHint: z.string().optional(), dependencies: z.array(directDependencySchema), }); diff --git a/src/github-issues/templates/issue-description.hbs b/src/github-issues/templates/issue-description.hbs index e98e0bd..ed54e8c 100644 --- a/src/github-issues/templates/issue-description.hbs +++ b/src/github-issues/templates/issue-description.hbs @@ -5,7 +5,7 @@ ## Summary {{#if ownerLabel}} -- {{ownerLabel}} +- **Owner:** {{ownerLabel}} {{/if}} {{#if multiVersion}} - **Current versions in monorepo:** diff --git a/src/linear/issueDescriptions.test.ts b/src/linear/issueDescriptions.test.ts index e7ed60c..0559b75 100644 --- a/src/linear/issueDescriptions.test.ts +++ b/src/linear/issueDescriptions.test.ts @@ -278,7 +278,7 @@ describe('buildIssueDescription', () => { getDetailUrl: testGetDetailUrl, providerInfo: npmProviderInfo, }); - expect(result).toContain('- Web App (Frontend)'); + expect(result).toContain('- **Owner:** Web App (Frontend)'); }); it('shows catalog How to Update', () => { diff --git a/src/linear/templates/issue-description.hbs b/src/linear/templates/issue-description.hbs index 46e15e9..de41c31 100644 --- a/src/linear/templates/issue-description.hbs +++ b/src/linear/templates/issue-description.hbs @@ -5,7 +5,7 @@ ## Summary {{#if ownerLabel}} -- {{ownerLabel}} +- **Owner:** {{ownerLabel}} {{/if}} {{#if multiVersion}} - **Current versions in monorepo:** diff --git a/src/security/SecurityPlugin.test.ts b/src/security/SecurityPlugin.test.ts index 132595d..2e36c07 100644 --- a/src/security/SecurityPlugin.test.ts +++ b/src/security/SecurityPlugin.test.ts @@ -137,7 +137,7 @@ describe('SecurityPlugin', () => { severity: 'high', advisoryCount: 2, fixAvailable: true, - rationale: ['2 advisories', 'fix available in a newer version'], + rationale: ['2 advisories', 'Fix available in a newer version'], sourceLinks: [{ label: 'GHSA-1234', url: 'https://osv.dev/vulnerability/GHSA-1234' }], }; scoped.setVersionFact('react', '1.0.0', SECURITY_FINDINGS_KEY, [finding]); @@ -155,7 +155,7 @@ describe('SecurityPlugin', () => { const whyValue = whyCol.getValue(ctx); expect(whyValue).toContain(' { diff --git a/src/security/sources/OsvSource.ts b/src/security/sources/OsvSource.ts index 7fc18b0..e235322 100644 --- a/src/security/sources/OsvSource.ts +++ b/src/security/sources/OsvSource.ts @@ -241,7 +241,7 @@ export class OsvSource implements DataSource { rationale.push(`${vulns.length} advisories`); } if (anyFixAvailable) { - rationale.push('fix available in a newer version'); + rationale.push('Fix available in a newer version'); } const sourceLinks = vulns.map((v) => ({ diff --git a/src/security/sources/SnykSource.test.ts b/src/security/sources/SnykSource.test.ts index 0b3bc4a..14a1ef9 100644 --- a/src/security/sources/SnykSource.test.ts +++ b/src/security/sources/SnykSource.test.ts @@ -114,6 +114,21 @@ describe('toFinding', () => { expect(toFinding([issue])?.fixAvailable).toBe(true); }); + it('lists a fix version once when Snyk repeats it', () => { + // Snyk can name the same release from two coordinates of one advisory. + const issue: SnykIssue = { + id: 'SNYK-JS-THING-2', + attributes: { + effective_severity_level: 'medium', + coordinates: [ + { remedies: [{ details: { upgrade_package: '15.5.27,16.3.8' } }] }, + { remedies: [{ details: { upgrade_package: '16.3.8' } }] }, + ], + }, + }; + expect(toFinding([issue])?.advisories?.[0]?.fixVersions).toEqual(['15.5.27', '16.3.8']); + }); + it('links each advisory to its Snyk page', () => { expect(toFinding(gin)?.sourceLinks?.[0]?.url).toMatch( /^https:\/\/security\.snyk\.io\/vuln\/SNYK-GOLANG-/, diff --git a/src/security/sources/SnykSource.ts b/src/security/sources/SnykSource.ts index d77d44c..60426fa 100644 --- a/src/security/sources/SnykSource.ts +++ b/src/security/sources/SnykSource.ts @@ -143,7 +143,7 @@ function fixVersions(issue: SnykIssue): string[] { for (const remedy of coordinate.remedies ?? []) { for (const part of remedy.details?.upgrade_package?.split(',') ?? []) { const trimmed = part.trim(); - if (trimmed) versions.push(trimmed); + if (trimmed && !versions.includes(trimmed)) versions.push(trimmed); } } } @@ -300,7 +300,7 @@ export function toFinding( : `${ordered.length} Snyk advisories`, ]; if (maturity) rationale.push(`Snyk exploit maturity: ${maturity}`); - if (fixAvailable) rationale.push('fix available in a newer version'); + if (fixAvailable) rationale.push('Fix available in a newer version'); return { source: 'snyk',